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

Add browser integration tests using Playwright #222

Merged
merged 3 commits into from
Mar 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -64,9 +64,6 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases
node-version: [18.x, 20.x]
suite: [commonjs, esm, typescript]
exclude:
- suite: cloudflare-worker
node-version: 18.x # Only test Cloudflare suite with the latest Node version
fail-fast: false

steps:
@@ -84,6 +81,36 @@ jobs:
npm --prefix integration/${{ matrix.suite }} install "./${{ needs.build.outputs.tarball-name }}"
npm --prefix integration/${{ matrix.suite }} test

integration-browser:
needs: [test, build]
runs-on: ubuntu-latest

env:
REPLICATE_API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }}

strategy:
matrix:
browser: ["chromium", "firefox", "webkit"]
suite: ["browser"]
fail-fast: false

steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
name: package-tarball
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: |
cd integration/${{ matrix.suite }}
npm install
npm install "../../${{ needs.build.outputs.tarball-name }}"
npm exec -- playwright install ${{ matrix.browser }}
npm exec -- playwright install-deps ${{ matrix.browser }}
npm exec -- playwright test --browser ${{ matrix.browser }}

integration-edge:
needs: [test, build]
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ const {
withAutomaticRetries,
validateWebhook,
parseProgressFromLogs,
streamAsyncIterator,
} = require("./lib/util");

const accounts = require("./lib/accounts");
@@ -296,7 +297,8 @@ class Replicate {
fetch: this.fetch,
options: { signal },
});
yield* stream;

yield* streamAsyncIterator(stream);
} else {
throw new Error("Prediction does not support streaming");
}
3 changes: 3 additions & 0 deletions integration/browser/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package-lock=false
audit=false
fund=false
39 changes: 39 additions & 0 deletions integration/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Browser integration tests

Uses [`playwright`](https://playwright.dev/docs) to run a basic integration test against the three most common browser engines, Firefox, Chromium and WebKit.

It uses the `replicate/canary` model for the moment, which requires a Replicate API token available in the environment under `REPLICATE_API_TOKEN`.

The entire suite is a single `main()` function that calls a single model exercising the streaming API.

The test uses `esbuild` within the test generate a browser friendly version of the `index.js` file which is loaded into the given browser and calls the `main()` function asserting the response content.

## CORS

The Replicate API doesn't support Cross Origin Resource Sharing at this time. We work around this in Playwright by intercepting the request in a `page.route` handler. We don't modify the request/response, but this seems to work around the restriction.

## Setup

npm install

## Local

The following command will run the tests across all browsers.

npm test

To run against the default browser (chromium) run:

npm exec playwright test

Or, specify a browser with:

npm exec playwright test --browser firefox

## Debugging

Running `playwright test` with the `--debug` flag opens a browser window with a debugging interface, and a breakpoint set at the start of the test. It can also be connected directly to VSCode.

npm exec playwright test --debug

The browser.js file is injected into the page via a script tag, to be able to set breakpoints in this file you'll need to use a `debugger` statement and open the devtools in the spawned browser window before continuing the test suite.
22 changes: 22 additions & 0 deletions integration/browser/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Replicate from "replicate";

/**
* @param {string} - token the REPLICATE_API_TOKEN
*/
window.main = async (token) => {
const replicate = new Replicate({ auth: token });
const stream = replicate.stream(
"replicate/canary:30e22229542eb3f79d4f945dacb58d32001b02cc313ae6f54eef27904edf3272",
{
input: {
text: "Betty Browser",
},
}
);

const output = [];
for await (const event of stream) {
output.push(String(event));
}
return output.join("");
};
34 changes: 34 additions & 0 deletions integration/browser/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { test, expect } from "@playwright/test";
import { build } from "esbuild";

// Convert the source file from commonjs to a browser script.
const result = await build({
entryPoints: ["index.js"],
bundle: true,
platform: "browser",
external: ["node:crypto"],
write: false,
});
const source = new TextDecoder().decode(result.outputFiles[0].contents);

// https://playwright.dev/docs/network#modify-requests

test("browser", async ({ page }) => {
// Patch the API endpoint to work around CORS for now.
await page.route(
"https://api.replicate.com/v1/predictions",
async (route) => {
// Fetch original response.
const response = await route.fetch();
// Add a prefix to the title.
return route.fulfill({ response });
}
);

await page.addScriptTag({ content: source });
const result = await page.evaluate(
(token) => window.main(token),
[process.env.REPLICATE_API_TOKEN]
);
expect(result).toBe("hello there, Betty Browser");
});
19 changes: 19 additions & 0 deletions integration/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "replicate-app-browser",
"private": true,
"version": "0.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "playwright test --browser all"
},
"license": "ISC",
"dependencies": {
"replicate": "../../"
},
"devDependencies": {
"@playwright/test": "^1.42.1",
"esbuild": "^0.20.1"
}
}
3 changes: 3 additions & 0 deletions integration/browser/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({});
3 changes: 2 additions & 1 deletion lib/stream.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Attempt to use readable-stream if available, attempt to use the built-in stream module.

const ApiError = require("./error");
const { streamAsyncIterator } = require("./util");
const {
EventSourceParserStream,
} = require("../vendor/eventsource-parser/stream");
@@ -73,7 +74,7 @@ function createReadableStream({ url, fetch, options = {} }) {
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream());

for await (const event of stream) {
for await (const event of streamAsyncIterator(stream)) {
if (event.event === "error") {
controller.error(new Error(event.data));
break;
24 changes: 24 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
@@ -354,9 +354,33 @@ function parseProgressFromLogs(input) {
return null;
}

/**
* Helper to make any `ReadableStream` iterable, this is supported
* by most server runtimes but browsers still haven't implemented
* it yet.
* See: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility
*
* @template T
* @param {ReadableStream<T>} stream an instance of a `ReadableStream`
* @yields {T} a chunk/event from the stream
*/
async function* streamAsyncIterator(stream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}

module.exports = {
transformFileInputs,
validateWebhook,
withAutomaticRetries,
parseProgressFromLogs,
streamAsyncIterator,
};
6 changes: 1 addition & 5 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -5,9 +5,5 @@
"strict": true,
"allowJs": true
},
"exclude": [
"**/node_modules",
"integration/**"
]
"exclude": ["integration/**", "**/node_modules"]
}