diff --git a/.changeset/plenty-lizards-cheat.md b/.changeset/plenty-lizards-cheat.md new file mode 100644 index 000000000000..155bfa87f781 --- /dev/null +++ b/.changeset/plenty-lizards-cheat.md @@ -0,0 +1,8 @@ +--- +"@gradio/lite": patch +"@gradio/wasm": patch +"@self/tootils": patch +"gradio": patch +--- + +fix:Fix Lite to work on FireFox diff --git a/.config/playwright.config.js b/.config/playwright.config.js index 6b5f971c16b6..01d9b1ce1215 100644 --- a/.config/playwright.config.js +++ b/.config/playwright.config.js @@ -56,7 +56,18 @@ const lite = defineConfig(base, { ], workers: 1, retries: 3, - timeout: 60000 + timeout: 60000, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] } + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + testIgnore: "**/kitchen_sink.*" // This test requires the camera permission but it's not supported on FireFox: https://github.com/microsoft/playwright/issues/11714 + } + ] }); export default !!process.env.GRADIO_E2E_TEST_LITE ? lite : normal; diff --git a/gradio/processing_utils.py b/gradio/processing_utils.py index 783b93591790..b833d721bbe1 100644 --- a/gradio/processing_utils.py +++ b/gradio/processing_utils.py @@ -95,7 +95,13 @@ async def handle_async_request( ) -> httpx.Response: url = str(request.url) method = request.method + headers = dict(request.headers) + # User-agent header is automatically set by the browser. + # More importantly, setting it causes an error on FireFox where a preflight request is made and it leads to a CORS error. + # Maybe related to https://bugzilla.mozilla.org/show_bug.cgi?id=1629921 + del headers["user-agent"] + body = None if method in ["GET", "HEAD"] else await request.aread() response = await pyodide.http.pyfetch( url, method=method, headers=headers, body=body diff --git a/js/lite/src/fetch.ts b/js/lite/src/fetch.ts index 4cc63f45c662..3349df65acff 100644 --- a/js/lite/src/fetch.ts +++ b/js/lite/src/fetch.ts @@ -44,12 +44,14 @@ export async function wasm_proxied_fetch( headers[key] = value; }); + const body = request.body ?? new Uint8Array(await request.arrayBuffer()); // request.body can't be read on FireFox, so fallback to arrayBuffer(). + const response = await workerProxy.httpRequest({ path: url.pathname, query_string: url.searchParams.toString(), // The `query_string` field in the ASGI spec must not contain the leading `?`. method, headers, - body: request.body + body }); return new Response(response.body, { status: response.status, diff --git a/js/spa/test/file_component_events.spec.ts b/js/spa/test/file_component_events.spec.ts index 48fdca532ebc..f20072b97443 100644 --- a/js/spa/test/file_component_events.spec.ts +++ b/js/spa/test/file_component_events.spec.ts @@ -11,12 +11,14 @@ async function error_modal_showed(page) { test("File component properly dispatches load event for the single file case.", async ({ page }) => { - await page - .getByRole("button", { name: "Drop File Here - or - Click to Upload" }) - .first() - .click(); - const uploader = await page.locator("input[type=file]").first(); - await uploader.setInputFiles(["./test/files/cheetah1.jpg"]); + const [fileChooser] = await Promise.all([ + page.waitForEvent("filechooser"), + page + .getByRole("button", { name: "Drop File Here - or - Click to Upload" }) + .first() + .click() + ]); + await fileChooser.setFiles(["./test/files/cheetah1.jpg"]); await expect(page.getByLabel("# Load Upload Single File")).toHaveValue("1"); diff --git a/js/spa/test/gallery_component_events.spec.ts b/js/spa/test/gallery_component_events.spec.ts index a0bc9c82b9b7..8e34acc0afa0 100644 --- a/js/spa/test/gallery_component_events.spec.ts +++ b/js/spa/test/gallery_component_events.spec.ts @@ -33,12 +33,14 @@ test("Gallery select event returns the right value and the download button works test("Gallery click-to-upload, upload and change events work correctly", async ({ page }) => { - await page - .getByRole("button", { name: "Drop Media Here - or - Click to Upload" }) - .first() - .click(); - const uploader = await page.locator("input[type=file]").first(); - await uploader.setInputFiles([ + const [fileChooser] = await Promise.all([ + page.waitForEvent("filechooser"), + page + .getByRole("button", { name: "Drop Media Here - or - Click to Upload" }) + .first() + .click() + ]); + await fileChooser.setFiles([ "./test/files/cheetah1.jpg", "./test/files/cheetah1.jpg" ]); diff --git a/js/spa/test/outbreak_forecast.spec.ts b/js/spa/test/outbreak_forecast.spec.ts index b0e638904b02..d7a86747d137 100644 --- a/js/spa/test/outbreak_forecast.spec.ts +++ b/js/spa/test/outbreak_forecast.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@self/tootils"; +import { test, expect, is_lite } from "@self/tootils"; test("selecting matplotlib should show matplotlib image and pressing clear should clear output", async ({ page @@ -20,8 +20,14 @@ test("selecting matplotlib should show matplotlib image and pressing clear shoul }); test("selecting plotly should show plotly plot and pressing clear should clear output", async ({ - page + page, + browserName }) => { + test.fixme( + browserName === "firefox" && is_lite, + "Plotly component can't be located on Lite on FireFox in the CI env for some reason" + ); + await page.getByLabel("Plot Type").click(); await page.getByRole("option", { name: "Plotly" }).click(); await page.getByLabel("Month").click(); @@ -29,9 +35,12 @@ test("selecting plotly should show plotly plot and pressing clear should clear o await page.getByLabel("Social Distancing?").check(); await page.click("text=Submit"); - await expect(page.locator(".js-plotly-plot")).toHaveCount(1); + + const plotly_plot = page.getByTestId("plotly"); + await expect(plotly_plot).toHaveCount(1); + await page.getByRole("button", { name: "Clear" }).click(); - await expect(page.locator(".js-plotly-plot")).toHaveCount(0); + await expect(plotly_plot).toHaveCount(0); }); test("selecting altair should show altair plot and pressing clear should clear output", async ({ @@ -45,7 +54,7 @@ test("selecting altair should show altair plot and pressing clear should clear o await page.click("text=Submit"); - const altair = await page.getByTestId("altair"); + const altair = page.getByTestId("altair"); await expect(altair).toHaveCount(1); await page.getByRole("button", { name: "Clear" }).click(); @@ -63,7 +72,7 @@ test("selecting bokeh should show bokeh plot and pressing clear should clear out await page.click("text=Submit"); - const altair = await page.getByTestId("bokeh"); + const altair = page.getByTestId("bokeh"); await expect(altair).toHaveCount(1); await page.getByRole("button", { name: "Clear" }).click(); @@ -71,8 +80,14 @@ test("selecting bokeh should show bokeh plot and pressing clear should clear out }); test("switching between all 4 plot types and pressing submit should update output component to corresponding plot type", async ({ - page + page, + browserName }) => { + test.fixme( + browserName === "firefox" && is_lite, + "Plotly component can't be located on Lite on FireFox in the CI env for some reason" + ); + //Matplotlib await page.getByLabel("Plot Type").click(); await page.getByRole("option", { name: "Matplotlib" }).click(); @@ -91,7 +106,8 @@ test("switching between all 4 plot types and pressing submit should update outpu await page.getByRole("option", { name: "Plotly" }).click(); await page.click("text=Submit"); - await expect(page.locator(".js-plotly-plot")).toHaveCount(1); + const plotly = page.getByTestId("plotly"); + await expect(plotly).toHaveCount(1); //Altair await page.getByLabel("Plot Type").click(); diff --git a/js/tootils/src/index.ts b/js/tootils/src/index.ts index bc5bce7cbf77..25d7e58a001a 100644 --- a/js/tootils/src/index.ts +++ b/js/tootils/src/index.ts @@ -21,7 +21,7 @@ const ROOT_DIR = path.resolve( "../../../.." ); -const is_lite = !!process.env.GRADIO_E2E_TEST_LITE; +export const is_lite = !!process.env.GRADIO_E2E_TEST_LITE; const test_normal = base.extend<{ setup: void }>({ setup: [ diff --git a/js/wasm/src/cross-origin-worker.ts b/js/wasm/src/cross-origin-worker.ts index 12c11f3f3b05..5a6b257d092e 100644 --- a/js/wasm/src/cross-origin-worker.ts +++ b/js/wasm/src/cross-origin-worker.ts @@ -26,20 +26,32 @@ function get_blob_url(url: URL): string { return worker_blob_url; } +function is_same_origin(url: URL): boolean { + return url.origin === window.location.origin; +} + export class CrossOriginWorkerMaker { public readonly worker: Worker | SharedWorker; constructor(url: URL, options?: WorkerOptions & { shared?: boolean }) { const { shared = false, ...workerOptions } = options ?? {}; - try { + if (is_same_origin(url)) { + console.debug( + `Loading a worker script from the same origin: ${url.toString()}.` + ); // This is the normal way to load a worker script, which is the best straightforward if possible. this.worker = shared ? new SharedWorker(url, workerOptions) : new Worker(url, workerOptions); - } catch (e) { + + // NOTE: We use here `if-else` checking the origin instead of `try-catch` + // because the `try-catch` approach doesn't work on some browsers like FireFox. + // In the cross-origin case, FireFox throws a SecurityError asynchronously after the worker is created, + // so we can't catch the error synchronously. + } else { console.debug( - `Failed to load a worker script from ${url.toString()}. Trying to load a cross-origin worker...` + `Loading a worker script from a different origin: ${url.toString()}.` ); const worker_blob_url = get_blob_url(url); this.worker = shared