Class: Puppeteer::WaitTask

Inherits:
Object
  • Object
show all
Defined in:
lib/puppeteer/wait_task.rb

Defined Under Namespace

Classes: TerminatedError, TimeoutError

Constant Summary collapse

WAIT_FOR_PREDICATE_PAGE_FUNCTION =
<<~JAVASCRIPT
function _(root, predicateBody, polling, ...args) {
    const predicate = new Function('...args', predicateBody);
    const observedRoot = root || document;
    if (polling === 'mutation' && typeof MutationObserver === 'undefined') {
        polling = 'raf';
    }

    function createDeferred() {
        let resolve;
        let reject;
        let finished = false;
        const promise = new Promise((res, rej) => {
            resolve = res;
            reject = rej;
        });
        return {
            promise,
            resolve: (value) => {
                if (finished) return;
                finished = true;
                resolve(value);
            },
            reject: (error) => {
                if (finished) return;
                finished = true;
                reject(error);
            },
            finished: () => finished,
        };
    }

    class MutationPoller {
        constructor(fn, root) {
            this.fn = fn;
            this.root = root;
            this.observer = null;
            this.deferred = null;
        }
        async start() {
            this.deferred = createDeferred();
            const result = await this.fn();
            if (result) {
                this.deferred.resolve(result);
                return;
            }
            this.observer = new MutationObserver(async () => {
                const result = await this.fn();
                if (!result) {
                    return;
                }
                this.deferred.resolve(result);
                await this.stop();
            });
            this.observer.observe(this.root, {
                childList: true,
                subtree: true,
                attributes: true,
            });
        }
        async stop() {
            if (!this.deferred) {
                return;
            }
            if (!this.deferred.finished()) {
                this.deferred.reject(new Error('Polling stopped'));
            }
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
        }
        result() {
            if (!this.deferred) {
                return Promise.reject(new Error('Polling never started'));
            }
            return this.deferred.promise;
        }
    }

    class RAFPoller {
        constructor(fn) {
            this.fn = fn;
            this.deferred = null;
            this.rafId = null;
        }
        async start() {
            this.deferred = createDeferred();
            const result = await this.fn();
            if (result) {
                this.deferred.resolve(result);
                return;
            }
            const poll = async () => {
                if (!this.deferred || this.deferred.finished()) {
                    return;
                }
                const result = await this.fn();
                if (result) {
                    this.deferred.resolve(result);
                    await this.stop();
                } else {
                    this.rafId = requestAnimationFrame(poll);
                }
            };
            this.rafId = requestAnimationFrame(poll);
        }
        async stop() {
            if (!this.deferred) {
                return;
            }
            if (!this.deferred.finished()) {
                this.deferred.reject(new Error('Polling stopped'));
            }
            if (this.rafId) {
                cancelAnimationFrame(this.rafId);
                this.rafId = null;
            }
        }
        result() {
            if (!this.deferred) {
                return Promise.reject(new Error('Polling never started'));
            }
            return this.deferred.promise;
        }
    }

    class IntervalPoller {
        constructor(fn, ms) {
            this.fn = fn;
            this.ms = ms;
            this.interval = null;
            this.deferred = null;
        }
        async start() {
            this.deferred = createDeferred();
            const result = await this.fn();
            if (result) {
                this.deferred.resolve(result);
                return;
            }
            this.interval = setInterval(async () => {
                const result = await this.fn();
                if (!result) {
                    return;
                }
                this.deferred.resolve(result);
                await this.stop();
            }, this.ms);
        }
        async stop() {
            if (!this.deferred) {
                return;
            }
            if (!this.deferred.finished()) {
                this.deferred.reject(new Error('Polling stopped'));
            }
            if (this.interval) {
                clearInterval(this.interval);
                this.interval = null;
            }
        }
        result() {
            if (!this.deferred) {
                return Promise.reject(new Error('Polling never started'));
            }
            return this.deferred.promise;
        }
    }

    const runner = () => predicate(...args);
    let poller;
    if (polling === 'raf') {
        poller = new RAFPoller(runner);
    } else if (polling === 'mutation') {
        poller = new MutationPoller(runner, observedRoot);
    } else if (typeof polling === 'number') {
        poller = new IntervalPoller(runner, polling);
    } else {
        throw new Error('Unknown polling option: ' + polling);
    }
    poller.start();
    return poller;
}
JAVASCRIPT

Instance Method Summary collapse

Constructor Details

#initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil, root: nil) ⇒ WaitTask

Returns a new instance of WaitTask.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/puppeteer/wait_task.rb', line 12

def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil, root: nil)
  if polling.is_a?(String)
    if polling != 'raf' && polling != 'mutation'
      raise ArgumentError.new("Unknown polling option: #{polling}")
    end
  elsif polling.is_a?(Numeric)
    if polling < 0
      raise ArgumentError.new("Cannot poll with non-positive interval: #{polling}")
    end
  else
    raise ArgumentError.new("Unknown polling options: #{polling}")
  end

  @dom_world = dom_world
  @polling = polling
  @timeout = timeout
  @root = root
  @predicate_body = build_predicate_body(predicate_body)
  @args = args
  @binding_function = binding_function
  @run_count = 0
  @dom_world.task_manager.add(self)
  if binding_function
    @dom_world.send(:_bound_functions)[binding_function.name] = binding_function
  end
  @promise = Async::Promise.new
  @poller_handle = nil
  @generic_error = Puppeteer::Error.new('Waiting failed')

  # Since page navigation requires us to re-install the pageScript, we should track
  # timeout on our end.
  if timeout && timeout > 0
    timeout_error = TimeoutError.new(timeout: timeout)
    @timeout_task = Async do |task|
      task.sleep(timeout / 1000.0)
      # Avoid stopping the timeout task from inside terminate/cleanup.
      @timeout_task = nil
      terminate(timeout_error) unless @timeout_cleared
    end
  end

  async_rerun
end

Instance Method Details

#await_promisePuppeteer::JSHandle

Returns:



57
58
59
# File 'lib/puppeteer/wait_task.rb', line 57

def await_promise
  @promise.wait
end

#rerunObject



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/puppeteer/wait_task.rb', line 71

def rerun
  run_count = (@run_count += 1)
  context = nil
  success = nil
  error = nil

  return if @terminated || run_count != @run_count
  reset_poller
  begin
    context = @dom_world.execution_context
    if @binding_function
      @dom_world.add_binding_to_context(context, @binding_function)
    end
    return if @terminated || run_count != @run_count

    @poller_handle = context.evaluate_handle(
      WAIT_FOR_PREDICATE_PAGE_FUNCTION,
      @root,
      @predicate_body,
      @polling,
      *@args,
    )
    success = @poller_handle.evaluate_handle('poller => poller.result()')
  rescue => err
    error = err
  end

  return if @terminated || run_count != @run_count

  if error
    bad_error = get_bad_error(error)
    if bad_error
      @generic_error.cause = bad_error
      terminate(@generic_error)
    else
      reset_poller
    end
    return
  end

  @promise.resolve(success) unless @promise.resolved?
  cleanup
end

#terminate(error = nil) ⇒ Object



61
62
63
64
65
66
67
68
69
# File 'lib/puppeteer/wait_task.rb', line 61

def terminate(error = nil)
  return if @terminated

  @terminated = true
  if error && !@promise.resolved?
    @promise.reject(error)
  end
  cleanup
end