From 1ee83bed56804b9541d5886bdec9f5e55580d8f2 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 11:04:30 -0500 Subject: [PATCH 01/26] Catch a typeerror on search results --- src/pages/app/Anonymous.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/app/Anonymous.svelte b/src/pages/app/Anonymous.svelte index 0251846c7..4942f5669 100644 --- a/src/pages/app/Anonymous.svelte +++ b/src/pages/app/Anonymous.svelte @@ -80,7 +80,7 @@

- {$_("anonymous.p1", { values: { n: $search.results.count } })} + {$_("anonymous.p1", { values: { n: $search?.results?.count ?? 0 } })}

{@html $_("anonymous.p2")} From 476c54fc390753383fa84ea621d6c31ea06cc0bc Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 11:06:24 -0500 Subject: [PATCH 02/26] handle undefined params --- src/pages/app/Documents.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/app/Documents.svelte b/src/pages/app/Documents.svelte index ae2266282..80604eb31 100644 --- a/src/pages/app/Documents.svelte +++ b/src/pages/app/Documents.svelte @@ -300,7 +300,7 @@ on:files={showUploadModal} disabled={embed || !$orgsAndUsers.loggedIn || !$orgsAndUsers.isVerified} > - {#if !$orgsAndUsers.loggedIn && $search.params.query === "" && !anonymousClosed} + {#if !$orgsAndUsers.loggedIn && $search.params?.query === "" && !anonymousClosed} {:else} {#each $documents.documents as document (document.id)} From 003f089271ebd87ecbb7634d3c2cd1aaa09b9051 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 14:11:41 -0500 Subject: [PATCH 03/26] Minimal working playwright setup --- .gitignore | 1 + package-lock.json | 16 +++ package.json | 12 +- playwright.config.js | 50 +++++++ tests/README.md | 9 ++ tests/anonymous/manager/app.spec.js | 9 ++ tests/anonymous/pages/home.spec.js | 9 ++ tests/{functional => }/fixtures/Small pdf.pdf | Bin .../the-nature-of-the-firm-CPEC11.pdf | Bin tests/functional/cases/access-tests.js | 89 ------------ tests/functional/cases/auth-tests.js | 23 --- tests/functional/cases/crud-tests.js | 136 ------------------ tests/functional/harness.js | 82 ----------- tests/functional/suites/noindex.js | 102 ------------- tests/functional/test-utils.js | 33 ----- 15 files changed, 101 insertions(+), 470 deletions(-) create mode 100644 playwright.config.js create mode 100644 tests/README.md create mode 100644 tests/anonymous/manager/app.spec.js create mode 100644 tests/anonymous/pages/home.spec.js rename tests/{functional => }/fixtures/Small pdf.pdf (100%) rename tests/{functional => }/fixtures/the-nature-of-the-firm-CPEC11.pdf (100%) delete mode 100644 tests/functional/cases/access-tests.js delete mode 100644 tests/functional/cases/auth-tests.js delete mode 100644 tests/functional/cases/crud-tests.js delete mode 100644 tests/functional/harness.js delete mode 100644 tests/functional/suites/noindex.js delete mode 100644 tests/functional/test-utils.js diff --git a/.gitignore b/.gitignore index e5bce719b..e2a59a3be 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ public/assets/ # Local test env file .env.test .env.sentry-build-plugin +playwright-report .vscode diff --git a/package-lock.json b/package-lock.json index bc98e7fd4..77a73e919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "webpack-dev-server": "^4.15.1" }, "devDependencies": { + "@playwright/test": "^1.39.0", "@storybook/addon-essentials": "^7.4.5", "@storybook/addon-interactions": "^7.4.5", "@storybook/addon-links": "^7.4.5", @@ -3576,6 +3577,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "dependencies": { + "playwright": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.23", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", diff --git a/package.json b/package.json index 2bdfb20da..c4d073dde 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "webpack-dev-server": "^4.15.1" }, "devDependencies": { + "@playwright/test": "^1.39.0", "@storybook/addon-essentials": "^7.4.5", "@storybook/addon-interactions": "^7.4.5", "@storybook/addon-links": "^7.4.5", @@ -83,16 +84,17 @@ "build-staging": "cross-env NODE_ENV=staging webpack", "build-serve": "cross-env NODE_ENV=production webpack && serve public -l 80 --single", "build-analyze": "cross-env NODE_ENV=production-analyze webpack", + "build-storybook": "storybook build", "dev": "concurrently \"npm:dev-app\" \"npm:dev-embed\"", "dev-app": "cross-env NODE_ENV=development SUPPRESS_WARNINGS=1 webpack serve --config webpack.app.config.js", "dev-embed": "cross-env NODE_ENV=development webpack --config webpack.embed.config.js", - "test": "NODE_OPTIONS=--experimental-vm-modules jest", "serve": "serve public -l 80 --single", - "watch-app": "cross-env NODE_ENV=development webpack watch --config webpack.app.config.js", - "watch": "concurrently npm:serve npm:dev-embed npm:watch-app", - "test-watch": "jest --watchAll", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test-watch": "jest --watchAll", + "test:browser": "playwright test", + "watch": "concurrently npm:serve npm:dev-embed npm:watch-app", + "watch-app": "cross-env NODE_ENV=development webpack watch --config webpack.app.config.js" }, "engine": 18, "browserslist": "> 0.25%, not dead", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..27c29b69c --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,50 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + // Look for test files in the "tests" directory, relative to this configuration file. + testDir: "tests", + + // Run all tests in parallel. + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only. + retries: process.env.CI ? 2 : 0, + + // Reporter to use + reporter: "html", + + use: { + // Base URL to use in actions like `await page.goto('/')`. + baseURL: process.env.URL || "https://www.dev.documentcloud.org", + + // Collect trace when retrying the failed test. + trace: "on-first-retry", + }, + + // Options specific to each project. + projects: [ + { + name: "chromium", + use: devices["Desktop Chrome"], + }, + { + name: "firefox", + use: devices["Desktop Firefox"], + }, + { + name: "webkit", + use: devices["Desktop Safari"], + }, + { + name: "Mobile Chrome", + use: devices["Pixel 5"], + }, + { + name: "Mobile Safari", + use: devices["iPhone 12"], + }, + ], +}); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..c3e497fce --- /dev/null +++ b/tests/README.md @@ -0,0 +1,9 @@ +# End-to-end tests + +This directory includes [Playwright](https://playwright.dev) tests that run in a headless browser against a running version of the site. To run locally, start a full instance (Squarelet, the DocumentCloud API and frontend) and set the `URL` environment variable. For example: + +```sh +URL=https://www.dev.documentcloud.org npx playwright test +``` + +Netlify will automatically run this against any pull request using a deploy preview, and it will set the `URL` environment variable to the correct target. diff --git a/tests/anonymous/manager/app.spec.js b/tests/anonymous/manager/app.spec.js new file mode 100644 index 000000000..3cabd7974 --- /dev/null +++ b/tests/anonymous/manager/app.spec.js @@ -0,0 +1,9 @@ +// @ts-check + +import { test, expect } from "@playwright/test"; + +test("basic manager rendering", async ({ page }) => { + await page.goto("/app"); + + await expect(page).toHaveTitle("DocumentCloud"); +}); diff --git a/tests/anonymous/pages/home.spec.js b/tests/anonymous/pages/home.spec.js new file mode 100644 index 000000000..08cf99803 --- /dev/null +++ b/tests/anonymous/pages/home.spec.js @@ -0,0 +1,9 @@ +// @ts-check + +import { test, expect } from "@playwright/test"; + +test("basic homepage test", async ({ page }) => { + await page.goto("/home"); + + await expect(page).toHaveTitle("Home | DocumentCloud"); +}); diff --git a/tests/functional/fixtures/Small pdf.pdf b/tests/fixtures/Small pdf.pdf similarity index 100% rename from tests/functional/fixtures/Small pdf.pdf rename to tests/fixtures/Small pdf.pdf diff --git a/tests/functional/fixtures/the-nature-of-the-firm-CPEC11.pdf b/tests/fixtures/the-nature-of-the-firm-CPEC11.pdf similarity index 100% rename from tests/functional/fixtures/the-nature-of-the-firm-CPEC11.pdf rename to tests/fixtures/the-nature-of-the-firm-CPEC11.pdf diff --git a/tests/functional/cases/access-tests.js b/tests/functional/cases/access-tests.js deleted file mode 100644 index 563f4b260..000000000 --- a/tests/functional/cases/access-tests.js +++ /dev/null @@ -1,89 +0,0 @@ -/* global process */ -var { openDoc } = require("../test-utils"); - -async function setHiddenPropInAccessDialogTest({ - harness, - t, - shouldHide = true, - testDocName, - appURL, -}) { - try { - var page = await harness.getOnlyPage(); - if (!page) { - return; - } - - var hideCheck = await page.locator(".hide-from-search-checkbox"); - await hideCheck[shouldHide ? "check" : "uncheck"](); - //await page.screenshot({ path: "after-checking.png", fullPage: true }); - - var accessDialog = await page.locator(".modalcontainer .modal"); - var changeButton = await page - .locator(".modalcontainer button[type='submit']") - .filter({ hasText: "Change" }); - await Promise.all([ - changeButton.click(), - accessDialog.waitFor({ state: "detached" }), - ]); - //await page.screenshot({ path: "after-change-button.png", fullPage: true }); - - await openDoc({ page, harness, docName: testDocName, appURL }); - //await page.screenshot({ path: "reopeneddoc.png", fullPage: true }); - - var robotsMetaTag = await page.locator("meta[name='robots']"); - - if (shouldHide) { - // This waitFor is necessary. I think it's because for page.locator() may default - // to waiting until the element is visible, but meta tags never are. - await robotsMetaTag.waitFor({ state: "attached" }); - const tagCount = await robotsMetaTag.count(); - t.equal(tagCount, 1, "Document has a meta tag for robots"); - t.equal( - await robotsMetaTag.getAttribute("content"), - "noindex", - 'meta tag content is set to "noindex"', - ); - } else { - try { - await robotsMetaTag.waitFor({ state: "attached", timeout: 5000 }); - } catch (e) { - // TODO: Find out if there is a less strange way to test for the absence - // of a DOM element. - if (/locator\.waitFor: Timeout \d+ms exceeded/.test(e.message)) { - t.pass("Document has no meta tag for robots"); - } else { - t.fail("Unexpected exception while confirming lack of robots tag."); - console.log( - `Unexpected exception while confirming lack of robots tag: ${e.message}, ${e.stack}`, - ); - } - } - } - } catch (error) { - t.fail(`Error opening doc: ${error.message}\n${error.stack}\n`); - process.exit(1); - } -} - -async function openAccessDialogFromViewerTest({ harness, browser, t }) { - try { - var page = await harness.getOnlyPage({ browser, t }); - if (!page) { - return; - } - var accessLink = await page.getByText("Public access"); - await accessLink.click(); - //await harness.stall(60000); - } catch (error) { - t.fail( - `Error opening access dialog from viewer: ${error.message}\n${error.stack}\n`, - ); - process.exit(1); - } -} - -module.exports = { - openAccessDialogFromViewerTest, - setHiddenPropInAccessDialogTest, -}; diff --git a/tests/functional/cases/auth-tests.js b/tests/functional/cases/auth-tests.js deleted file mode 100644 index 2f7e8efbc..000000000 --- a/tests/functional/cases/auth-tests.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global process */ - -async function signInTest({ page, t }) { - if (!process.env) { - throw new Error( - "TEST_USER and TEST_PASS need to be set as environment variables and dotenv needs to be initialized before calling signInTest.", - ); - } - try { - await page.getByText("Sign in").click({ strict: false }); - await page.locator("#id_login").fill(process.env.TEST_USER); - await page.locator("#id_password").fill(process.env.TEST_PASS); - var form = await page.locator("#login_form"); - var logInButton = await form.getByText("Log in"); - await logInButton.click(); - t.pass("Signed in"); - } catch (error) { - t.fail(`Error while signing in: ${error.message}\n${error.stack}\n`); - process.exit(1); - } -} - -module.exports = { signInTest }; diff --git a/tests/functional/cases/crud-tests.js b/tests/functional/cases/crud-tests.js deleted file mode 100644 index 0bf549808..000000000 --- a/tests/functional/cases/crud-tests.js +++ /dev/null @@ -1,136 +0,0 @@ -/* global process */ - -var { - getOpenButtonForDoc, - getURLForDocByName, - openDoc, -} = require("../test-utils"); - -async function uploadTest({ harness, t, testDocName }) { - try { - var page = await harness.getOnlyPage(); - if (!page) { - return; - } - - var buttons = await page.locator("button"); - var uploadButton = await buttons.filter({ hasText: /Upload/ }); - await uploadButton.click(); - - var selectFilesButton = await buttons.filter({ hasText: /Select files/ }); - - const [fileChooser] = await Promise.all([ - page.waitForEvent("filechooser"), - selectFilesButton.click(), - ]); - - await fileChooser.setFiles(`tests/functional/fixtures/${testDocName}.pdf`); - - var publicButton = await page.getByText( - "Document will be publicly visible.", - ); - await publicButton.click(); - - var beginButton = await page.getByText("Begin upload"); - await beginButton.click(); - - var openDocButton = await getOpenButtonForDoc({ - managerPage: page, - docName: testDocName, - }); - t.ok( - openDocButton && (await openDocButton.count()) === 1, - "Open button appears for uploaded doc.", - ); - t.pass("Document uploaded."); - } catch (error) { - t.fail(`Error uploading: ${error.message}\n${error.stack}\n`); - process.exit(1); - } -} - -async function deleteDocTest({ harness, browser, t, appURL, testDocName }) { - try { - var page = await harness.getOnlyPage({ browser, t }); - if (!page) { - return; - } - await page.goto(appURL); - await page.waitForURL((url) => url.href.startsWith(appURL)); - - const docURL = await getURLForDocByName({ - docName: testDocName, - page, - harness, - appURL, - }); - - var docRows = await page.locator(".card .row"); - var docRowWithURL; - const rowCount = await docRows.count(); - for (let i = 0; i < rowCount; ++i) { - let docRow = docRows.nth(i); - let link = await docRow.locator(`a[href="${docURL}"]`); - const hitCount = await link.count(); - // As of 2022-11-10, there will be at least two matches if this is the - // correct row: One for the text link and one for the image link. - if (hitCount > 0) { - docRowWithURL = docRow; - break; - } - } - - if (!docRowWithURL) { - t.fail("Found row with target document URL."); - return; - } - - //var docCheckBox = await docRowWithURL.getByRole("check"); - var docCheckBoxSpan = await docRowWithURL.locator( - 'input[type="checkbox"] ~ span', - ); - // We have a trick checkbox. The actual input is hidden (opacity 0, under a span, - // so we actually need to click the span (which intercepts pointer events) - // to trigger the check. - await docCheckBoxSpan.click(); - - var editSpan = await page - .locator(".barcontainer .action") - .getByText("Edit"); - await editSpan.click(); - var deleteDiv = await page - .locator(".barcontainer .menu") - .getByText("Delete"); - await deleteDiv.click(); - var deleteButton = await page - .locator(".modalcontainer button[type='submit']") - .filter({ hasText: "Delete" }); - await deleteButton.click(); - t.pass("Document deleted without errors."); - // TODO: Make sure the document is actually gone? - } catch (error) { - t.fail(`Error opening doc: ${error.message}\n${error.stack}\n`); - process.exit(1); - } -} - -async function openDocTest({ harness, t, testDocName, appURL }) { - try { - var page = await harness.getOnlyPage(); - if (!page) { - return; - } - - const slugifiedDocName = testDocName.replace(/ /g, "-"); - await openDoc({ page, harness, docName: testDocName, appURL }); - t.ok( - page.url().endsWith(slugifiedDocName.toLowerCase()), - "Navigated to the document's page.", - ); - } catch (error) { - t.fail(`Error opening doc: ${error.message}\n${error.stack}\n`); - process.exit(1); - } -} - -module.exports = { uploadTest, deleteDocTest, openDocTest }; diff --git a/tests/functional/harness.js b/tests/functional/harness.js deleted file mode 100644 index 8c4aa663f..000000000 --- a/tests/functional/harness.js +++ /dev/null @@ -1,82 +0,0 @@ -/* global process */ - -// A convienience wrapper for testing with the headless browsers. - -var playwright = require("playwright"); - -function Harness({ startURL, browserType = "webkit" }) { - var browser; - - return { - setUp, - tearDown, - goWaitForURL, - stall, - loadClick, - getOnlyPage, - }; - - async function setUp() { - var config = { - headless: !process.env.DEBUG, - //args: [ - //"--webview-enable-modern-cookie-same-site", - //"--ignore-certificate-errors", - //"--ignore-certificate-errors-skip-list", - //"--allow-cross-origin-auth-prompt", - //"--allow-external-pages", - //"--allow-failed-policy-fetch-for-test", - //"--allow-running-insecure-content", - //], - }; - browser = await playwright[browserType].launch(config); - var page = await browser.newPage({ ignoreHTTPSErrors: true }); - await page.goto(startURL); - return { browser, page }; - } - - async function tearDown() { - await Promise.all(browser.contexts().map((context) => context.close())); - return browser.close(); - } - - async function goWaitForURL({ page, url }) { - page.goto(url); - return page.waitForURL((currentURL) => currentURL.href.startsWith(url)); - } - - function stall(time) { - return new Promise(callSetTimeout); - - function callSetTimeout(resolve) { - setTimeout(resolve, time); - } - } - - function loadClick(page, clickable) { - return Promise.all([ - page.waitForNavigation({ waitUntil: "domcontentloaded" }), - clickable.click(), - ]); - } - - async function getOnlyPage() { - var contexts = await browser.contexts(); - if (contexts.length !== 1) { - throw new Error( - `There is more than one context. (Actual number of contexts: ${contexts.length})`, - ); - } - - var pages = await contexts[0].pages(); - if (pages.length !== 1) { - throw new Error( - `There is more than one page. (Actual number of pages: ${pages.length})`, - ); - } - - return pages[0]; - } -} - -module.exports = Harness; diff --git a/tests/functional/suites/noindex.js b/tests/functional/suites/noindex.js deleted file mode 100644 index 7ba063736..000000000 --- a/tests/functional/suites/noindex.js +++ /dev/null @@ -1,102 +0,0 @@ -/* global process, __dirname */ - -var test = require("tape"); -var Harness = require("../harness"); -var path = require("path"); - -// In Docker, .env files won't be there, but the actual environment variables -// will have been set by local.builder.yml. -var envFilename = ".env.test"; -if (process.argv.length > 3 && process.argv[2] === "--envfile") { - envFilename = process.argv[3]; -} -require("dotenv").config({ - path: path.join(__dirname, `../../../${envFilename}`), -}); - -var { signInTest } = require("../cases/auth-tests.js"); -var { - uploadTest, - openDocTest, - deleteDocTest, -} = require("../cases/crud-tests.js"); -var { - openAccessDialogFromViewerTest, - setHiddenPropInAccessDialogTest, -} = require("../cases/access-tests.js"); - -const browserType = process.env.BROWSER || "webkit"; -const testDocName = "Small pdf"; -const baseURL = process.env.APP_URL; -const appURL = baseURL + "app"; - -// TODO when another suite is added: Abstract the harness setup and teardown and the env setup. -(async () => { - try { - var harness = Harness({ - // TODO: Grab from env. - startURL: baseURL, - browserType, - }); - var { browser, page } = await harness.setUp(); - var base = { harness, browser, page, appURL, testDocName }; - await runTest({ ...base, name: "Sign-in test", testBody: signInTest }); - await runTest({ ...base, name: "Upload test", testBody: uploadTest }); - await runTest({ ...base, name: "Open doc test", testBody: openDocTest }); - await runTest({ - ...base, - name: "Open access dialog from viewer", - testBody: openAccessDialogFromViewerTest, - }); - await runTest({ - ...base, - name: "Make document hidden in access dialog", - testBody: setHiddenPropInAccessDialogTest, - }); - await runTest({ - ...base, - name: "Open access dialog from viewer again", - testBody: openAccessDialogFromViewerTest, - }); - await runTest({ - ...base, - name: "Make document NOT hidden in access dialog", - testBody: setHiddenPropInAccessDialogTest, - shouldHide: false, - }); - } catch (error) { - console.error(error, error.stack); - } finally { - // Test deleting document regardless of what happens - // so the next run is clean. - await runTest({ - ...base, - name: "Delete uploaded documents", - testBody: deleteDocTest, - }); - await harness.tearDown(browser); - } -})(); - -// TODO, if we have a lot of time: TypeScript. -function runTest(opts) { - return new Promise(executor); - - function executor(resolve, reject) { - var testBody = opts.testBody; - delete opts.testBody; - - test(opts.name, waitForTestBody); - - async function waitForTestBody(t) { - try { - await testBody({ ...opts, t }); - t.end(); - resolve(); - } catch (error) { - t.end(); - reject(error); - } - } - } -} diff --git a/tests/functional/test-utils.js b/tests/functional/test-utils.js deleted file mode 100644 index 7413a7725..000000000 --- a/tests/functional/test-utils.js +++ /dev/null @@ -1,33 +0,0 @@ -// Browser manipulation utils for testing. - -async function getOpenButtonForDoc({ managerPage, docName }) { - var docCard = await managerPage.locator(".docscontainer .card", { - hasText: docName, - }); - await docCard.waitFor(); - - var button = docCard.locator("button", { hasText: "Open" }); - await button.waitFor({ timeout: 180000 }); - return button; -} - -async function openDoc({ harness, page, docName, appURL }) { - await harness.goWaitForURL({ page, url: appURL }); - var openDocButton = await getOpenButtonForDoc({ managerPage: page, docName }); - await harness.loadClick(page, openDocButton); -} - -async function getURLForDocByName({ harness, page, docName, appURL }) { - await harness.goWaitForURL({ page, url: appURL }); - var openDocButton = await getOpenButtonForDoc({ managerPage: page, docName }); - // Playwright does not expose parentElement or parentNode. - var openDocLink = await openDocButton.locator("../.."); - await openDocLink.waitFor(); - return openDocLink.getAttribute("href"); -} - -module.exports = { - openDoc, - getOpenButtonForDoc, - getURLForDocByName, -}; From 8adbc2f0cb8c12ed9554000ef1558c7403930cf5 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 14:27:49 -0500 Subject: [PATCH 04/26] Add a workflow to run unit tests on PRs --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..1beece4dc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Test + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + NODE_ENV: production + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: "18.x" + - run: npm ci + - run: npm run build --if-present + - run: npm test From 2717a31acebe4d1e6bb8e8dfbf58a782708f9046 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 14:32:46 -0500 Subject: [PATCH 05/26] Only NODE_ENV=production to build --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1beece4dc..e09fe1e56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,6 @@ on: pull_request: branches: [master] -env: - NODE_ENV: production - jobs: build: runs-on: ubuntu-latest @@ -24,4 +21,6 @@ jobs: node-version: "18.x" - run: npm ci - run: npm run build --if-present + env: + NODE_ENV: production - run: npm test From 8b1ebe1ae452d5f084266f622e1d2b918e2fce69 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 14:43:39 -0500 Subject: [PATCH 06/26] Make jest and playwright ignore each other --- jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 0964fdfb8..70fed85f6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,10 +2,10 @@ export default { extensionsToTreatAsEsm: [".svelte"], moduleNameMapper: { - "^@/(.*)$": "/src/$1", + "^@/(.*)$": "/$1", }, moduleFileExtensions: ["js", "svelte"], - modulePaths: ["src"], + rootDir: "src", setupFiles: ["dotenv/config"], testEnvironment: "jsdom", transform: { From 8920dc64dc5f20cedf1e20e16cdab16991ecbf38 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 15:57:51 -0500 Subject: [PATCH 07/26] Try deployment notifications --- .github/workflows/playwright.yml | 23 +++++++++++++++++++++++ .github/workflows/test.yml | 2 +- playwright.config.js | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..dfe0569f0 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,23 @@ +name: Playwright Tests + +on: + deployment_status: + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + env: + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e09fe1e56..37c823012 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Test +name: Unit tests on: push: diff --git a/playwright.config.js b/playwright.config.js index 27c29b69c..6af25a4c9 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -16,6 +16,8 @@ export default defineConfig({ // Reporter to use reporter: "html", + workers: process.env.CI ? 1 : undefined, + use: { // Base URL to use in actions like `await page.goto('/')`. baseURL: process.env.URL || "https://www.dev.documentcloud.org", From 360883339319376d1e27827286cd5fccc7c97fa1 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 16:04:25 -0500 Subject: [PATCH 08/26] Try a netlify plugin --- .github/workflows/playwright.yml | 1 + netlify.toml | 2 ++ plugins/test/index.js | 6 ++++++ plugins/test/manifest.yml | 1 + 4 files changed, 10 insertions(+) create mode 100644 netlify.toml create mode 100644 plugins/test/index.js create mode 100644 plugins/test/manifest.yml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index dfe0569f0..d2ab1cfb8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -21,3 +21,4 @@ jobs: run: npx playwright test env: PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} + URL: ${{ github.event.deployment_status.target_url }} diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..b9f256257 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,2 @@ +[[context.deploy-preview.plugins]] +package = "/plugins/test" \ No newline at end of file diff --git a/plugins/test/index.js b/plugins/test/index.js new file mode 100644 index 000000000..8aac8f94a --- /dev/null +++ b/plugins/test/index.js @@ -0,0 +1,6 @@ +// netlify plugin to run playwright on deploy previews + +export async function onSuccess({ utils }) { + await utils.run("playwright", ["install"]); + await utils.run("playwright", ["test"]); +} diff --git a/plugins/test/manifest.yml b/plugins/test/manifest.yml new file mode 100644 index 000000000..4fdd17086 --- /dev/null +++ b/plugins/test/manifest.yml @@ -0,0 +1 @@ +name: netlify-plugin-playwright From 8d3751adba6ca7fb671ae0406193f4dc5667e84b Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 16:10:14 -0500 Subject: [PATCH 09/26] deps --- plugins/test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/test/index.js b/plugins/test/index.js index 8aac8f94a..6a0389112 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -1,6 +1,6 @@ // netlify plugin to run playwright on deploy previews export async function onSuccess({ utils }) { - await utils.run("playwright", ["install"]); + await utils.run("playwright", ["install", "--with-deps"]); await utils.run("playwright", ["test"]); } From a9ce06582cafc98dfaddacc0b53b73a3cbc9eff5 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 16:54:13 -0500 Subject: [PATCH 10/26] deps alone --- plugins/test/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/test/index.js b/plugins/test/index.js index 6a0389112..fc7b3ee2b 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -1,6 +1,7 @@ // netlify plugin to run playwright on deploy previews export async function onSuccess({ utils }) { - await utils.run("playwright", ["install", "--with-deps"]); + await utils.run("playwright", ["install-deps"]); + await utils.run("playwright", ["install"]); await utils.run("playwright", ["test"]); } From 7a85fe5f0e7ba797e6951c3f091a097a5d31bfb9 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Tue, 14 Nov 2023 16:58:44 -0500 Subject: [PATCH 11/26] logging --- plugins/test/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/test/index.js b/plugins/test/index.js index fc7b3ee2b..7cb16afec 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -1,7 +1,12 @@ // netlify plugin to run playwright on deploy previews export async function onSuccess({ utils }) { + console.log("Installing Playwright dependencies"); await utils.run("playwright", ["install-deps"]); await utils.run("playwright", ["install"]); + + console.log("Running Playwright tests"); await utils.run("playwright", ["test"]); + + console.log("Done."); } From f3fc1e8c34fe3e5d48b97b805623237a35e3f9d9 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 10:18:46 -0500 Subject: [PATCH 12/26] Just chromium for now --- playwright.config.js | 2 ++ plugins/test/index.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 6af25a4c9..1503494ba 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -32,6 +32,7 @@ export default defineConfig({ name: "chromium", use: devices["Desktop Chrome"], }, + /* { name: "firefox", use: devices["Desktop Firefox"], @@ -48,5 +49,6 @@ export default defineConfig({ name: "Mobile Safari", use: devices["iPhone 12"], }, + */ ], }); diff --git a/plugins/test/index.js b/plugins/test/index.js index 7cb16afec..24022af3a 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -2,8 +2,7 @@ export async function onSuccess({ utils }) { console.log("Installing Playwright dependencies"); - await utils.run("playwright", ["install-deps"]); - await utils.run("playwright", ["install"]); + await utils.run("playwright", ["install", "--with-deps", "chromium"]); console.log("Running Playwright tests"); await utils.run("playwright", ["test"]); From dbcc22882f0f41fcf0d2071cbd7f839abf8deb89 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 13:18:05 -0500 Subject: [PATCH 13/26] Add cache, remove projects --- netlify.toml | 3 ++- package-lock.json | 7 +++++++ package.json | 1 + playwright.config.js | 4 ++-- plugins/test/index.js | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/netlify.toml b/netlify.toml index b9f256257..0479b5010 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,2 +1,3 @@ [[context.deploy-preview.plugins]] -package = "/plugins/test" \ No newline at end of file + package = "netlify-plugin-playwright-cache" + package = "/plugins/test" diff --git a/package-lock.json b/package-lock.json index 77a73e919..44c0dc7eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "jest-environment-jsdom": "^29.7.0", "msw": "^1.2.3", "msw-storybook-addon": "^1.8.0", + "netlify-plugin-playwright-cache": "^0.0.1", "playwright": "^1.39.0", "prettier": "^3.0.2", "prettier-plugin-svelte": "^3.0.3", @@ -17626,6 +17627,12 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/netlify-plugin-playwright-cache": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/netlify-plugin-playwright-cache/-/netlify-plugin-playwright-cache-0.0.1.tgz", + "integrity": "sha512-FECC4DtoYKpGGUNevxXVTE0GdD5LYSWwTPJpHv/WW4VlviTle8QlKBvF3Exfm48TDvZCUR3ofX1Zz+cJWOWBWA==", + "dev": true + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", diff --git a/package.json b/package.json index c4d073dde..0bbd20136 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "jest-environment-jsdom": "^29.7.0", "msw": "^1.2.3", "msw-storybook-addon": "^1.8.0", + "netlify-plugin-playwright-cache": "^0.0.1", "playwright": "^1.39.0", "prettier": "^3.0.2", "prettier-plugin-svelte": "^3.0.3", diff --git a/playwright.config.js b/playwright.config.js index 1503494ba..f8349e8c9 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -27,12 +27,12 @@ export default defineConfig({ }, // Options specific to each project. + /* projects: [ { name: "chromium", use: devices["Desktop Chrome"], }, - /* { name: "firefox", use: devices["Desktop Firefox"], @@ -49,6 +49,6 @@ export default defineConfig({ name: "Mobile Safari", use: devices["iPhone 12"], }, - */ ], + */ }); diff --git a/plugins/test/index.js b/plugins/test/index.js index 24022af3a..55e3fbc7e 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -2,7 +2,7 @@ export async function onSuccess({ utils }) { console.log("Installing Playwright dependencies"); - await utils.run("playwright", ["install", "--with-deps", "chromium"]); + await utils.run("playwright", ["install"]); console.log("Running Playwright tests"); await utils.run("playwright", ["test"]); From 12c3f2e2b1a8ba72fc9ccff9a3063fe065c27a72 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 13:19:38 -0500 Subject: [PATCH 14/26] separate --- netlify.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netlify.toml b/netlify.toml index 0479b5010..28af529a0 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,5 @@ [[context.deploy-preview.plugins]] package = "netlify-plugin-playwright-cache" + +[[context.deploy-preview.plugins]] package = "/plugins/test" From 37886d869881d6192246de56e3456cef69840b92 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 14:54:31 -0500 Subject: [PATCH 15/26] Viewer test --- .github/workflows/test.yml | 4 +-- .gitignore | 1 + plugins/test/index.js | 12 ++++++--- tests/anonymous/viewer/document.spec.js | 36 +++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tests/anonymous/viewer/document.spec.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37c823012..ecf7ec911 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: branches: [master] jobs: - build: + test: runs-on: ubuntu-latest steps: @@ -20,7 +20,7 @@ jobs: with: node-version: "18.x" - run: npm ci - - run: npm run build --if-present + - run: npm run build env: NODE_ENV: production - run: npm test diff --git a/.gitignore b/.gitignore index e2a59a3be..86da8a731 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ public/assets/ .env.test .env.sentry-build-plugin playwright-report +test-results .vscode diff --git a/plugins/test/index.js b/plugins/test/index.js index 55e3fbc7e..401e44169 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -2,10 +2,16 @@ export async function onSuccess({ utils }) { console.log("Installing Playwright dependencies"); - await utils.run("playwright", ["install"]); + await utils.run("playwright", ["install"]).catch((err) => { + utils.build.failBuild(err); + }); console.log("Running Playwright tests"); - await utils.run("playwright", ["test"]); + result = await utils.run("playwright", ["test"]).catch((err) => { + utils.build.failBuild(err); + }); - console.log("Done."); + utils.status.show({ + title: "Playwright tests completed.", + }); } diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js new file mode 100644 index 000000000..842f86bc6 --- /dev/null +++ b/tests/anonymous/viewer/document.spec.js @@ -0,0 +1,36 @@ +// @ts-check + +import { test, expect } from "@playwright/test"; + +test("test", async ({ page }) => { + // this is on staging; don't delete this document + const title = "FINALSeasonal_allergies_pollen_and_mold_2023__EN_"; + const url = + "/documents/20000007-finalseasonal_allergies_pollen_and_mold_2023__en"; + await page.goto(url); + + await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); + + await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); + + await expect(page.locator("h1")).toHaveText(title); + + await page.getByRole("link", { name: "p. 1" }).click(); + + const currentUrl = new URL(page.url()); + + expect(currentUrl.hash).toEqual("#document/p1"); + + await page + .locator("div") + .filter({ hasText: /^DocumentPlain TextThumbnailSearch Results$/ }) + .getByRole("combobox") + .selectOption("text"); + + // check that text view loaded + await expect(page.locator(".text").first()).toHaveText(/^MARCH 2023/); + + // switch to thumbnail view, click the first image + await page.getByRole("combobox").selectOption("thumbnail"); + await page.locator("img").first().click(); +}); From affb51dcc5c7a650ee5325f2c07214355b9ff6dd Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 15:06:24 -0500 Subject: [PATCH 16/26] failPlugin not failBuild --- plugins/test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/test/index.js b/plugins/test/index.js index 401e44169..91ed3dbaf 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -8,7 +8,7 @@ export async function onSuccess({ utils }) { console.log("Running Playwright tests"); result = await utils.run("playwright", ["test"]).catch((err) => { - utils.build.failBuild(err); + utils.build.failPlugin(err); }); utils.status.show({ From 92c9162bfd34b1b708e33e331e3209ac9ed3dcc8 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Wed, 15 Nov 2023 16:03:29 -0500 Subject: [PATCH 17/26] status --- plugins/test/index.js | 3 +++ tests/anonymous/viewer/document.spec.js | 1 + 2 files changed, 4 insertions(+) diff --git a/plugins/test/index.js b/plugins/test/index.js index 91ed3dbaf..3c5915448 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -8,6 +8,9 @@ export async function onSuccess({ utils }) { console.log("Running Playwright tests"); result = await utils.run("playwright", ["test"]).catch((err) => { + utils.status.show({ + title: "Playwright test failed", + }); utils.build.failPlugin(err); }); diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 842f86bc6..1e690ed02 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -3,6 +3,7 @@ import { test, expect } from "@playwright/test"; test("test", async ({ page }) => { + console.log(page.url()); // this is on staging; don't delete this document const title = "FINALSeasonal_allergies_pollen_and_mold_2023__EN_"; const url = From d233e92bcee9980da35f38191634fdaca0db3189 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Thu, 16 Nov 2023 09:55:16 -0500 Subject: [PATCH 18/26] Trying a GH action --- .github/workflows/playwright.yml | 24 +++++++++++++++++++----- plugins/test/index.js | 2 ++ tests/anonymous/manager/app.spec.js | 3 +++ tests/anonymous/viewer/document.spec.js | 1 - 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d2ab1cfb8..d9c0c1fd5 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,13 +1,28 @@ name: Playwright Tests on: - deployment_status: + pull_request: + branches: [master] + +env: + URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" jobs: + wait: + runs-on: ubuntu-latest + + steps: + - uses: cygnetdigital/wait_for_response@v2.0.0 + with: + url: ${{ env.URL }} + responseCode: "200" + timeout: 120000 + interval: 3000 + test: timeout-minutes: 60 runs-on: ubuntu-latest - if: github.event.deployment_status.state == 'success' + steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -15,10 +30,9 @@ jobs: node-version: 18 - name: Install dependencies run: npm ci + - name: Install Playwright run: npx playwright install --with-deps + - name: Run Playwright tests run: npx playwright test - env: - PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} - URL: ${{ github.event.deployment_status.target_url }} diff --git a/plugins/test/index.js b/plugins/test/index.js index 3c5915448..20b813120 100644 --- a/plugins/test/index.js +++ b/plugins/test/index.js @@ -10,11 +10,13 @@ export async function onSuccess({ utils }) { result = await utils.run("playwright", ["test"]).catch((err) => { utils.status.show({ title: "Playwright test failed", + summary: err.toString(), }); utils.build.failPlugin(err); }); utils.status.show({ title: "Playwright tests completed.", + summary: "", }); } diff --git a/tests/anonymous/manager/app.spec.js b/tests/anonymous/manager/app.spec.js index 3cabd7974..5d02c4903 100644 --- a/tests/anonymous/manager/app.spec.js +++ b/tests/anonymous/manager/app.spec.js @@ -5,5 +5,8 @@ import { test, expect } from "@playwright/test"; test("basic manager rendering", async ({ page }) => { await page.goto("/app"); + const url = new URL(page.url()); + expect(url.pathname).toBe("/app"); + await expect(page).toHaveTitle("DocumentCloud"); }); diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 1e690ed02..842f86bc6 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -3,7 +3,6 @@ import { test, expect } from "@playwright/test"; test("test", async ({ page }) => { - console.log(page.url()); // this is on staging; don't delete this document const title = "FINALSeasonal_allergies_pollen_and_mold_2023__EN_"; const url = From 39a7f85fb8259e6aad0d6ca433b555d67003d8fc Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Thu, 16 Nov 2023 09:58:00 -0500 Subject: [PATCH 19/26] needs wait --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d9c0c1fd5..f6fdf0a4a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -22,6 +22,7 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + needs: wait steps: - uses: actions/checkout@v3 From 3c4282dc9d1f4669ca7ffdf276482ddb0e696c07 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Thu, 16 Nov 2023 11:34:58 -0500 Subject: [PATCH 20/26] try to set the right base url --- .github/workflows/playwright.yml | 4 ++++ netlify.toml | 5 ----- tests/anonymous/viewer/document.spec.js | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 netlify.toml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f6fdf0a4a..6ca28afc3 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,11 +1,14 @@ name: Playwright Tests on: + push: + branches: [master] pull_request: branches: [master] env: URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" + PLAYWRIGHT_TEST_BASE_URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" jobs: wait: @@ -29,6 +32,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 + - name: Install dependencies run: npm ci diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 28af529a0..000000000 --- a/netlify.toml +++ /dev/null @@ -1,5 +0,0 @@ -[[context.deploy-preview.plugins]] - package = "netlify-plugin-playwright-cache" - -[[context.deploy-preview.plugins]] - package = "/plugins/test" diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 842f86bc6..88f99167d 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -9,6 +9,8 @@ test("test", async ({ page }) => { "/documents/20000007-finalseasonal_allergies_pollen_and_mold_2023__en"; await page.goto(url); + expect(new URL(page.url()).pathname).toBe(url); + await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); @@ -17,9 +19,7 @@ test("test", async ({ page }) => { await page.getByRole("link", { name: "p. 1" }).click(); - const currentUrl = new URL(page.url()); - - expect(currentUrl.hash).toEqual("#document/p1"); + expect(new URL(page.url()).hash).toEqual("#document/p1"); await page .locator("div") From 3ae110b361230d6a56c58764c739b7558a272b82 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Thu, 16 Nov 2023 11:49:29 -0500 Subject: [PATCH 21/26] this test man --- tests/anonymous/viewer/document.spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 88f99167d..7eada2a70 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -9,9 +9,9 @@ test("test", async ({ page }) => { "/documents/20000007-finalseasonal_allergies_pollen_and_mold_2023__en"; await page.goto(url); - expect(new URL(page.url()).pathname).toBe(url); + console.log(page.url()); - await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); + expect(new URL(page.url()).pathname).toBe(url); await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); @@ -33,4 +33,6 @@ test("test", async ({ page }) => { // switch to thumbnail view, click the first image await page.getByRole("combobox").selectOption("thumbnail"); await page.locator("img").first().click(); + + await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); }); From 93cfc0b7180e1fe99ffb4253f9af751dd0f26d8d Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Thu, 16 Nov 2023 14:57:40 -0500 Subject: [PATCH 22/26] Use a document on staging --- tests/anonymous/viewer/document.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 7eada2a70..a4c6a6a57 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -2,15 +2,17 @@ import { test, expect } from "@playwright/test"; -test("test", async ({ page }) => { +test("basic document test", async ({ page }) => { // this is on staging; don't delete this document const title = "FINALSeasonal_allergies_pollen_and_mold_2023__EN_"; const url = - "/documents/20000007-finalseasonal_allergies_pollen_and_mold_2023__en"; + "/documents/20005908-finalseasonal_allergies_pollen_and_mold_2023__en"; await page.goto(url); console.log(page.url()); + await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); + expect(new URL(page.url()).pathname).toBe(url); await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); @@ -33,6 +35,4 @@ test("test", async ({ page }) => { // switch to thumbnail view, click the first image await page.getByRole("combobox").selectOption("thumbnail"); await page.locator("img").first().click(); - - await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); }); From d597d1c11786ef0428eca9116242ba85b9fef81f Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Fri, 17 Nov 2023 12:02:23 -0500 Subject: [PATCH 23/26] Test on an available public document --- playwright.config.js | 15 ++++- tests/anonymous/viewer/document.spec.js | 73 +++++++++++++++++-------- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index f8349e8c9..990b256c0 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,4 +1,15 @@ import { defineConfig, devices } from "@playwright/test"; +import dotenv from "dotenv"; + +const environment = process.env.NODE_ENV || "development"; + +dotenv.config({ + path: environment === "development" ? ".env" : `.env.${environment}`, +}); + +if (environment === "development") { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} export default defineConfig({ // Look for test files in the "tests" directory, relative to this configuration file. @@ -27,7 +38,6 @@ export default defineConfig({ }, // Options specific to each project. - /* projects: [ { name: "chromium", @@ -41,6 +51,7 @@ export default defineConfig({ name: "webkit", use: devices["Desktop Safari"], }, + /* todo configure tests for mobile { name: "Mobile Chrome", use: devices["Pixel 5"], @@ -49,6 +60,6 @@ export default defineConfig({ name: "Mobile Safari", use: devices["iPhone 12"], }, + */ ], - */ }); diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index a4c6a6a57..24c55f74b 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -2,37 +2,64 @@ import { test, expect } from "@playwright/test"; -test("basic document test", async ({ page }) => { - // this is on staging; don't delete this document - const title = "FINALSeasonal_allergies_pollen_and_mold_2023__EN_"; - const url = - "/documents/20005908-finalseasonal_allergies_pollen_and_mold_2023__en"; - await page.goto(url); +const DC_BASE = process.env.DC_BASE; - console.log(page.url()); +let document, text; - await expect(page.locator(".sidebar").getByRole("heading")).toHaveText(title); +test.describe("document tests", () => { + // fetch the first available public document as a test case + test.beforeAll(async () => { + const endpoint = new URL( + "api/documents.json?access=public&per_page=1", + DC_BASE, + ); - expect(new URL(page.url()).pathname).toBe(url); + const { results } = await fetch(endpoint) + .then((r) => r.json()) + .catch(console.error); - await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); + document = results[0]; - await expect(page.locator("h1")).toHaveText(title); + console.log(`Using test document: ${document.title}`); - await page.getByRole("link", { name: "p. 1" }).click(); + const textEndpoint = new URL( + `documents/${document.id}/${document.slug}.txt.json`, + document.asset_url, + ); - expect(new URL(page.url()).hash).toEqual("#document/p1"); + text = await fetch(textEndpoint).then((r) => r.json()); + }); - await page - .locator("div") - .filter({ hasText: /^DocumentPlain TextThumbnailSearch Results$/ }) - .getByRole("combobox") - .selectOption("text"); + test("basic document test", async ({ page }) => { + await page.goto(document.canonical_url); - // check that text view loaded - await expect(page.locator(".text").first()).toHaveText(/^MARCH 2023/); + expect(page.url()).toBe(document.canonical_url); - // switch to thumbnail view, click the first image - await page.getByRole("combobox").selectOption("thumbnail"); - await page.locator("img").first().click(); + await expect(page.locator(".sidebar").getByRole("heading")).toHaveText( + document.title, + ); + + await page.getByRole("link", { name: "Original Document (PDF) »" }).click(); + + await expect(page.locator("h1")).toHaveText(document.title); + + await page.getByRole("link", { name: "p. 1" }).click(); + + expect(new URL(page.url()).hash).toEqual("#document/p1"); + + await page + .locator("div") + .filter({ hasText: /^DocumentPlain TextThumbnailSearch Results$/ }) + .getByRole("combobox") + .selectOption("text"); + + // check that text view loaded + await expect(page.locator(".text").first()).toHaveText( + text.pages[0].contents, + ); + + // switch to thumbnail view, click the first image + await page.getByRole("combobox").selectOption("thumbnail"); + await page.locator("img").first().click(); + }); }); From ce0bdd689a5f08caa165c5c009722f59c70c8d8f Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Fri, 17 Nov 2023 12:05:33 -0500 Subject: [PATCH 24/26] NODE_ENV --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6ca28afc3..5c1c21e23 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -9,6 +9,7 @@ on: env: URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" PLAYWRIGHT_TEST_BASE_URL: "https://deploy-preview-${{ github.event.number }}.muckcloud.com" + NODE_ENV: staging jobs: wait: From 23045c660a659d8b05422ccde0926d68957c9d7a Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Fri, 17 Nov 2023 12:14:02 -0500 Subject: [PATCH 25/26] logging --- tests/anonymous/viewer/document.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index 24c55f74b..fb9a4001f 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -27,7 +27,12 @@ test.describe("document tests", () => { document.asset_url, ); - text = await fetch(textEndpoint).then((r) => r.json()); + text = await fetch(textEndpoint) + .then((r) => r.json()) + .catch((e) => { + console.error(e); + console.error(textEndpoint); + }); }); test("basic document test", async ({ page }) => { From af8d693a0fbb58e033e9d47128c52493bb2cee32 Mon Sep 17 00:00:00 2001 From: Chris Amico Date: Mon, 20 Nov 2023 12:48:14 -0500 Subject: [PATCH 26/26] Use document fixtures --- .gitignore | 3 + tests/anonymous/viewer/document.spec.js | 68 +++++----- tests/fixtures/production.json | 158 ++++++++++++++++++++++++ tests/fixtures/staging.json | 60 +++++++++ 4 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 tests/fixtures/production.json create mode 100644 tests/fixtures/staging.json diff --git a/.gitignore b/.gitignore index 86da8a731..7fad13404 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ public/assets/ playwright-report test-results +# local fixtures, everyone should generate their own +tests/fixtures/development.json + .vscode scratch/ diff --git a/tests/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js index fb9a4001f..01c11840b 100644 --- a/tests/anonymous/viewer/document.spec.js +++ b/tests/anonymous/viewer/document.spec.js @@ -1,44 +1,34 @@ // @ts-check - -import { test, expect } from "@playwright/test"; - -const DC_BASE = process.env.DC_BASE; - -let document, text; - -test.describe("document tests", () => { - // fetch the first available public document as a test case - test.beforeAll(async () => { - const endpoint = new URL( - "api/documents.json?access=public&per_page=1", - DC_BASE, +import fs from "node:fs/promises"; +import { test as base, expect } from "@playwright/test"; + +const { + DC_BASE = "https://api.dev.documentcloud.org", + NODE_ENV = "development", +} = process.env; + +const test = base.extend({ + document: async ({ page }, use) => { + const filename = new URL( + `../../fixtures/${NODE_ENV}.json`, + import.meta.url, ); - const { results } = await fetch(endpoint) - .then((r) => r.json()) - .catch(console.error); - - document = results[0]; - - console.log(`Using test document: ${document.title}`); + const documents = await fs + .readFile(filename) + .then((s) => JSON.parse(s.toString())); - const textEndpoint = new URL( - `documents/${document.id}/${document.slug}.txt.json`, - document.asset_url, - ); - - text = await fetch(textEndpoint) - .then((r) => r.json()) - .catch((e) => { - console.error(e); - console.error(textEndpoint); - }); - }); + await use(documents[0]); + }, +}); - test("basic document test", async ({ page }) => { - await page.goto(document.canonical_url); +test.describe("document tests", () => { + test("basic document test", async ({ page, document }) => { + // canonical will point to a URL that might not exist on staging + const path = new URL(document.canonical_url).pathname; + await page.goto(path).catch(console.error); - expect(page.url()).toBe(document.canonical_url); + expect(new URL(page.url()).pathname).toBe(path); await expect(page.locator(".sidebar").getByRole("heading")).toHaveText( document.title, @@ -59,9 +49,11 @@ test.describe("document tests", () => { .selectOption("text"); // check that text view loaded - await expect(page.locator(".text").first()).toHaveText( - text.pages[0].contents, - ); + /* + await expect(page.locator(".text").first()).toHaveText( + text.pages[0].contents, + ); + */ // switch to thumbnail view, click the first image await page.getByRole("combobox").selectOption("thumbnail"); diff --git a/tests/fixtures/production.json b/tests/fixtures/production.json new file mode 100644 index 000000000..8de6c2d71 --- /dev/null +++ b/tests/fixtures/production.json @@ -0,0 +1,158 @@ +[ + { + "id": 1, + "access": "public", + "admin_noindex": false, + "asset_url": "https://s3.documentcloud.org/", + "canonical_url": "https://www.documentcloud.org/documents/1-a-i-g-bailout-the-inspector-generals-report", + "created_at": "2010-02-22T19:48:08.738905Z", + "data": {}, + "description": "Neil Barofsky's report concludes that officials overseeing the rescue of the American International Group might have overpaid other banks to wrap up A.I.G.'s financial obligations.", + "edit_access": false, + "file_hash": "", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 47, + "page_spec": "612.0x792.0:0-46", + "projects": [ + 46386, + 6 + ], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "a-i-g-bailout-the-inspector-generals-report", + "source": "Office of the Special Inspector General for T.A.R.P.", + "status": "success", + "title": "A.I.G. Bailout: The Inspector General's Report", + "updated_at": "2020-11-10T16:23:31.154198Z", + "user": 1 + }, + { + "id": 2, + "access": "public", + "admin_noindex": false, + "asset_url": "https://s3.documentcloud.org/", + "canonical_url": "https://www.documentcloud.org/documents/2-president-obamas-health-care-proposal", + "created_at": "2010-02-22T19:57:44.131650Z", + "data": {}, + "description": "On Feb. 22, 2010, the Obama Administration released a detailed proposal outlining the President's plan for a compromise among the House and Senate versions of a health care bill, and Republican concerns.", + "edit_access": false, + "file_hash": "", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 11, + "page_spec": "612.0x792.0:0-10", + "projects": [], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "president-obamas-health-care-proposal", + "source": "whitehouse.gov", + "status": "success", + "title": "President Obama's Health Care Proposal", + "updated_at": "2020-11-10T16:23:31.180653Z", + "user": 1 + }, + { + "id": 3, + "access": "public", + "admin_noindex": false, + "asset_url": "https://s3.documentcloud.org/", + "canonical_url": "https://www.documentcloud.org/documents/3-shaping-the-next-economic-expansion", + "created_at": "2010-02-22T20:13:31.180904Z", + "data": {}, + "description": "A speech delivered by Lawrence Summers to the New York Economic Club on October 29, 2009. ", + "edit_access": false, + "file_hash": "", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 7, + "page_spec": "612.0x792.0:0-6", + "projects": [ + 46386, + 6 + ], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "shaping-the-next-economic-expansion", + "source": "New York Economic Club", + "status": "success", + "title": "Shaping the Next Economic Expansion", + "updated_at": "2020-11-10T16:23:31.160644Z", + "user": 1 + }, + { + "id": 4, + "access": "public", + "admin_noindex": false, + "asset_url": "https://s3.documentcloud.org/", + "canonical_url": "https://www.documentcloud.org/documents/4-inspector-generals-report-on-medicare-prescription-fraud", + "created_at": "2010-02-22T20:16:51.842446Z", + "data": {}, + "description": "Daniel Levinson's report concludes that federal health officials need to cooperate better with fraud investigations, and that the government should require private insurers to report all instances of suspected fraud.", + "edit_access": false, + "file_hash": "", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 31, + "page_spec": "612.0x792.0:0-26,28-30;616.7999877929688x795.1199951171875:27", + "projects": [ + 2, + 9352 + ], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "inspector-generals-report-on-medicare-prescription-fraud", + "source": "Department of Health and Human Services", + "status": "success", + "title": "Inspector General's Report on Medicare Prescription Fraud", + "updated_at": "2020-11-10T16:23:31.160353Z", + "user": 1 + }, + { + "id": 5, + "access": "public", + "admin_noindex": false, + "asset_url": "https://s3.documentcloud.org/", + "canonical_url": "https://www.documentcloud.org/documents/5-wiretapping-lawsuits-thrown-out", + "created_at": "2010-02-22T21:22:53.524125Z", + "data": {}, + "description": "In this ruling, Judge Vaughn R. Walker throws out dozens of lawsuits claiming that the nation's largest telecommunications companies illegally assisted in the government's wiretapping program.", + "edit_access": false, + "file_hash": "", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 46, + "page_spec": "612.0x792.0:0-45", + "projects": [ + 2 + ], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "wiretapping-lawsuits-thrown-out", + "source": "United States District Court for Northern California", + "status": "success", + "title": "Wiretapping Lawsuits Thrown Out", + "updated_at": "2020-11-10T16:23:31.159780Z", + "user": 1 + } +] diff --git a/tests/fixtures/staging.json b/tests/fixtures/staging.json new file mode 100644 index 000000000..2311e7adb --- /dev/null +++ b/tests/fixtures/staging.json @@ -0,0 +1,60 @@ +[ + { + "id": 20005908, + "access": "public", + "admin_noindex": false, + "asset_url": "https://documentcloud-staging-files.s3.amazonaws.com/", + "canonical_url": "https://www.staging.documentcloud.org/documents/20005908-finalseasonal_allergies_pollen_and_mold_2023__en", + "created_at": "2023-03-21T20:33:35.857475Z", + "data": {}, + "description": "", + "edit_access": false, + "file_hash": "f776f2148c685d7d67aef0082f1d430dfe4bbe17", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 9, + "page_spec": "612.0x792.0000610351562:0-8", + "projects": [], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "finalseasonal_allergies_pollen_and_mold_2023__en", + "source": "", + "status": "success", + "title": "FINALSeasonal_allergies_pollen_and_mold_2023__EN_", + "updated_at": "2023-09-18T16:56:04.839073Z", + "user": 100012 + }, + { + "id": 20006188, + "access": "public", + "admin_noindex": false, + "asset_url": "https://documentcloud-staging-files.s3.amazonaws.com/", + "canonical_url": "https://www.staging.documentcloud.org/documents/20006188-change-assessment_w_mjw-letter", + "created_at": "2023-10-25T17:39:22.192849Z", + "data": {}, + "description": "", + "edit_access": false, + "file_hash": "0ee61c995723d434b710f89d95a2681b56a0f30c", + "noindex": false, + "language": "eng", + "organization": 1, + "original_extension": "pdf", + "page_count": 24, + "page_spec": "612.0x792.0:0-23", + "projects": [], + "publish_at": null, + "published_url": "", + "related_article": "", + "revision_control": false, + "slug": "change-assessment_w_mjw-letter", + "source": "", + "status": "success", + "title": "Change-assessment_w_MJW-letter", + "updated_at": "2023-11-20T16:02:35.301271Z", + "user": 100012 + } +]