Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change runJS parameter passing and return value #535

Merged
merged 8 commits into from
Aug 1, 2022
Merged

Conversation

calebeby
Copy link
Member

@calebeby calebeby commented Jul 27, 2022

Closes #522

There are only like ~15 lines of code changes here! I added a lot of comments, tests, and documentation.

Recommend reviewing with whitespace changes hidden.

Overview of the changes in the changesets:

Values exported from runJS are now available in Node.

For example:

test(
  'receiving exported values from runJS',
  withBrowser(async ({ utils }) => {
    // Each export is available in the returned object.
    // Each export is wrapped in a JSHandle, meaning that it points to an in-browser object
    const { focusTarget, favoriteNumber } = await utils.runJS(`
      export const focusTarget = document.activeElement
      export const favoriteNumber = 20
    `);

    // Serializable JSHandles can be unwrapped using JSONValue:
    console.log(await favoriteNumber.jsonValue()); // Logs "20"

    // A JSHandle<Element>, or ElementHandle is not serializable
    // But we can pass it back into the browser to use it (it will be unwrapped in the browser):

    await utils.runJS(
      `
      // The import.meta.pleasantestArgs context object receives the parameters passed in below
      const [focusTarget] = import.meta.pleasantestArgs;
      console.log(focusTarget) // Logs the element in the browser
      `,
      // Passing the JSHandle in here passes it into the browser (unwrapped) in import.meta.pleasantestArgs
      [focusTarget],
    );
  }),
);

We've also introduced a utility function to make it easier to call JSHandles that point to functions, makeCallableJSHandle. This function takes a JSHandle<Function> and returns a node function that calls the corresponding browser function, passing along the parameters, and returning the return value wrapped in Promise<JSHandle<T>>:

// new import:
import { makeCallableJSHandle } from 'pleasantest';

test(
  'calling functions with makeCallableJSHandle',
  withBrowser(async ({ utils }) => {
    const { displayFavoriteNumber } = await utils.runJS(`
      export const displayFavoriteNumber = (number) => {
        document.querySelector('.output').innerHTML = "Favorite number is: " + number
      }
    `);

    // displayFavoriteNumber is a JSHandle<Function>
    // (a pointer to a function in the browser)
    // so we cannot call it directly, so we wrap it in a node function first:

    const displayFavoriteNumberNode = makeCallableJSHandle(
      displayFavoriteNumber,
    );

    // Note the added `await`.
    // Even though the original function was not async, the wrapped function is.
    // This is needed because the wrapped function needs to asynchronously communicate with the browser.
    await displayFavoriteNumberNode(42);
  }),
);

For TypeScript users, runJS now accepts a new optional type parameter, to specify the exported types of the in-browser module that is passed in. The default value for this parameter is Record<string, unknown> (an object with string properties and unknown values). Note that this type does not include JSHandles, those are wrapped in the return type from runJS automatically.

Using the first example, the optional type would be:

test(
  'receiving exported values from runJS',
  withBrowser(async ({ utils }) => {
    const { focusTarget, favoriteNumber } = await utils.runJS<{
      focusTarget: Element;
      favoriteNumber: number;
    }>(`
      export const focusTarget = document.activeElement
      export const favoriteNumber = 20
    `);
  }),
);

Now focusTarget automatically has the type JSHandle<Element> and favoriteNumber automatically has the type JSHandle<number>. Without passing in the type parameter to runJS, their types would both be JSHandle<unknown>.


The way that runJS receives parameters in the browser has changed. Now, parameters are available as import.meta.pleasantestArgs instead of through an automatically-called default export.

For example, code that used to work like this:

test(
  'old version of runJS parameters',
  withBrowser(async ({ utils }) => {
    // Pass a variable from node to the browser
    const url = isDev ? 'dev.example.com' : 'prod.example.com';

    await utils.runJS(
      `
      // Parameters get passed into the default-export function, which is called automatically
      export default (url) => {
        console.log(url)
      }
      `,
      // array of parameters passed here
      [url],
    );
  }),
);

Now should be written like this:

test(
  'new version of runJS parameters',
  withBrowser(async ({ utils }) => {
    // Pass a variable from node to the browser
    const url = isDev ? 'dev.example.com' : 'prod.example.com';

    await utils.runJS(
      `
      // Parameters get passed as an array into this context variable, and we can destructure them
      const [url] = import.meta.pleasantestArgs
      console.log(url)
      // If we added a default exported function here, it would no longer be automatically called.
      `,
      // array of parameters passed here
      [url],
    );
  }),
);

This is a breaking change, because the previous mechanism for receiving parameters no longer works, and functions that are default exports from runJS are no longer called automatically.

@calebeby calebeby marked this pull request as ready for review July 27, 2022 21:38
Copy link
Member

@spaceninja spaceninja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These code changes look good and align with what we talked about in the dev team meeting.

+1 from me, but I'd appreciate it if you get a +1 from @gerardo-rodriguez as well, since some of this stuff is a little over my head, and he's more familiar with the scenario that led to this change.

README.md Outdated Show resolved Hide resolved
Co-authored-by: Gerardo Rodriguez <mr.gerardo.rodriguez@gmail.com>
expect(result.default.toString()).toEqual('JSHandle:hi');

expect(await result.a.jsonValue()).toStrictEqual(25);
expect(await result.b.jsonValue()).toStrictEqual({}); // function is not JSON-serializable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment! 👍🏽

Copy link
Member

@gerardo-rodriguez gerardo-rodriguez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work, @calebeby, yay! 🎉

And the documentation is excellent! Thank you so much. 💯

@calebeby calebeby merged commit dc6f81c into main Aug 1, 2022
@calebeby calebeby deleted the runjs-return branch August 1, 2022 18:34
@github-actions github-actions bot mentioned this pull request Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Return exports from runJS, and pass in args via import.meta.pleasantestArgs
3 participants