diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..5c1c21e23 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,44 @@ +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" + NODE_ENV: staging + +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 + needs: wait + + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..ecf7ec911 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +# 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: Unit tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + 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 + env: + NODE_ENV: production + - run: npm test diff --git a/.gitignore b/.gitignore index e5bce719b..7fad13404 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,11 @@ public/assets/ # Local test env file .env.test .env.sentry-build-plugin +playwright-report +test-results + +# local fixtures, everyone should generate their own +tests/fixtures/development.json .vscode 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: { diff --git a/package-lock.json b/package-lock.json index bc98e7fd4..44c0dc7eb 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", @@ -73,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", @@ -3576,6 +3578,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", @@ -17610,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 2bdfb20da..0bbd20136 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", @@ -68,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", @@ -83,16 +85,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..990b256c0 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,65 @@ +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. + 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", + + 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", + + // 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"], + }, + /* todo configure tests for mobile + { + name: "Mobile Chrome", + use: devices["Pixel 5"], + }, + { + name: "Mobile Safari", + use: devices["iPhone 12"], + }, + */ + ], +}); diff --git a/plugins/test/index.js b/plugins/test/index.js new file mode 100644 index 000000000..20b813120 --- /dev/null +++ b/plugins/test/index.js @@ -0,0 +1,22 @@ +// netlify plugin to run playwright on deploy previews + +export async function onSuccess({ utils }) { + console.log("Installing Playwright dependencies"); + await utils.run("playwright", ["install"]).catch((err) => { + utils.build.failBuild(err); + }); + + console.log("Running Playwright tests"); + 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/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 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")} 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)} 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..5d02c4903 --- /dev/null +++ b/tests/anonymous/manager/app.spec.js @@ -0,0 +1,12 @@ +// @ts-check + +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/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/anonymous/viewer/document.spec.js b/tests/anonymous/viewer/document.spec.js new file mode 100644 index 000000000..01c11840b --- /dev/null +++ b/tests/anonymous/viewer/document.spec.js @@ -0,0 +1,62 @@ +// @ts-check +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 documents = await fs + .readFile(filename) + .then((s) => JSON.parse(s.toString())); + + await use(documents[0]); + }, +}); + +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(new URL(page.url()).pathname).toBe(path); + + 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(); + }); +}); 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/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 + } +] 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, -};