diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..90b6b700d --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 60ff6bb8c..76ec0182e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,8 @@ build-storybook.log storybook-static/ # cypress -cypress.env.json \ No newline at end of file +cypress.env.json +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9de8904..ffce029ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,42 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v0.83.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.83.0...v0.83.0) + +- fix(media): update routes [`#1862`](https://github.com/isomerpages/isomercms-frontend/pull/1862) + +#### [v0.83.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.82.0...v0.83.0) + +> 21 March 2024 + +- Test/repo privatisation [`#1342`](https://github.com/isomerpages/isomercms-frontend/pull/1342) +- fix(resourceModal): sane default permalink [`#1858`](https://github.com/isomerpages/isomercms-frontend/pull/1858) +- fix(routeselector/resourceroom): remove accidental comments + fix typo [`#1859`](https://github.com/isomerpages/isomercms-frontend/pull/1859) +- chore(validation): add validation for all routes [`#1854`](https://github.com/isomerpages/isomercms-frontend/pull/1854) +- fix(permalinks): default permalink for create [`#1843`](https://github.com/isomerpages/isomercms-frontend/pull/1843) +- feat(tiptap): add supersub [`#1855`](https://github.com/isomerpages/isomercms-frontend/pull/1855) +- fix(app): change service to `isomer` [`#1857`](https://github.com/isomerpages/isomercms-frontend/pull/1857) +- fix(accordion): styles [`#1834`](https://github.com/isomerpages/isomercms-frontend/pull/1834) +- fet(admin): add release prep script [`#1853`](https://github.com/isomerpages/isomercms-frontend/pull/1853) +- feat(playwright): migrate [`#1805`](https://github.com/isomerpages/isomercms-frontend/pull/1805) +- feat(editpage): add resource spec, remove cypress spec [`#1796`](https://github.com/isomerpages/isomercms-frontend/pull/1796) +- feat(editpage): add collection page tests [`#1795`](https://github.com/isomerpages/isomercms-frontend/pull/1795) +- feat(e2e): migrate unlinked page tests to playwright [`#1790`](https://github.com/isomerpages/isomercms-frontend/pull/1790) +- feat(playwright): migrate dashboard spec [`#1789`](https://github.com/isomerpages/isomercms-frontend/pull/1789) +- chore(deps-dev): bump browserify-sign from 4.2.1 to 4.2.3 [`#1836`](https://github.com/isomerpages/isomercms-frontend/pull/1836) +- chore(deps-dev): bump @adobe/css-tools from 4.2.0 to 4.3.3 [`#1837`](https://github.com/isomerpages/isomercms-frontend/pull/1837) +- chore(deps): bump follow-redirects from 1.15.4 to 1.15.6 [`#1849`](https://github.com/isomerpages/isomercms-frontend/pull/1849) +- chore(deps): bump react-select from 5.7.4 to 5.8.0 [`#1847`](https://github.com/isomerpages/isomercms-frontend/pull/1847) +- build(package-lock): bump pm to 2.0.8 to avoid legal [`#1833`](https://github.com/isomerpages/isomercms-frontend/pull/1833) +- chore(deps): bump the npm_and_yarn group group with 2 updates [`#1842`](https://github.com/isomerpages/isomercms-frontend/pull/1842) +- chore(deps-dev): bump @storybook/addon-actions from 7.1.1 to 8.0.0 [`#1841`](https://github.com/isomerpages/isomercms-frontend/pull/1841) +- backport v0.82.0 [`#1846`](https://github.com/isomerpages/isomercms-frontend/pull/1846) +- chore: bump version to v0.83.0 [`f981939`](https://github.com/isomerpages/isomercms-frontend/commit/f9819395c2078f3c21e639c7fc68d40abae4c9dc) + #### [v0.82.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.81.0...v0.82.0) +> 14 March 2024 + - chore(deps-dev): bump @babel/traverse from 7.22.8 to 7.23.2 [`#1599`](https://github.com/isomerpages/isomercms-frontend/pull/1599) - deps: fix package-lock.json [`#1840`](https://github.com/isomerpages/isomercms-frontend/pull/1840) - fix: package.json to reduce vulnerabilities [`#1816`](https://github.com/isomerpages/isomercms-frontend/pull/1816) @@ -21,6 +55,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat(app): add link between traces + rum [`#1832`](https://github.com/isomerpages/isomercms-frontend/pull/1832) - fix(no broken link): not reporting duplicate permalink [`#1831`](https://github.com/isomerpages/isomercms-frontend/pull/1831) - 0.81.0 [`#1830`](https://github.com/isomerpages/isomercms-frontend/pull/1830) +- chore: bump version to v0.82.0 [`566e5c8`](https://github.com/isomerpages/isomercms-frontend/commit/566e5c8190d4cbef3ed0d9e73ddbccd4006a6c74) #### [v0.81.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.80.0...v0.81.0) diff --git a/cypress/e2e/dashboard.spec.ts b/cypress/e2e/dashboard.spec.ts deleted file mode 100644 index 719757086..000000000 --- a/cypress/e2e/dashboard.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { closeReviewRequests } from "../api" -import { - E2E_EMAIL_REPO_MASTER_LINK, - E2E_EMAIL_REPO_STAGING_LINK, - E2E_EMAIL_TEST_SITE, - ISOMER_GUIDE_LINK, - TEST_REPO_NAME, -} from "../fixtures/constants" -import { getOpenStagingButton, visitE2eEmailTestRepo } from "../utils" - -const getReviewRequestButton = () => cy.contains("button", "Request a Review") - -const getOpenStagingDropdownButton = () => - getOpenStagingButton().siblings("button") - -const goToWorkspace = () => cy.contains("a", "Edit site").click() - -const REVIEW_MODAL_SUBTITLE = - "An Admin needs to review and approve your changes before they can be published" - -const REVIEW_REQUEST_ALERT_MESSAGE = - "There’s a Review request pending approval. Any changes you make now will be added to the existing Review request, and published with the changes in it." - -describe("dashboard flow", () => { - beforeEach(() => { - cy.setupDefaultInterceptors() - cy.setEmailSessionDefaults("Email admin") - visitE2eEmailTestRepo() - closeReviewRequests() - }) - - it('should open the staging site on click of the "Open staging" button', () => { - // Assert - getOpenStagingButton() - .should("have.attr", "href", E2E_EMAIL_REPO_STAGING_LINK) - .should("have.attr", "target", "_blank") - }) - - it('should open the "Request a Review" modal on click of the "Request a Review" button', () => { - // Act - getReviewRequestButton().click() - - // Assert - cy.contains(REVIEW_MODAL_SUBTITLE).should("be.visible") - }) - - it("should be able to navigate to the staging site using the dropdown button", () => { - // Act - getOpenStagingDropdownButton().click() - - // Assert - cy.contains("Open staging site") - .should("be.visible") - .should("have.attr", "href", E2E_EMAIL_REPO_STAGING_LINK) - .should("have.attr", "target", "_blank") - }) - - it("should be able to navigate to the production site using the dropdown button", () => { - // Act - getOpenStagingDropdownButton().click() - - // Assert - cy.contains("Visit live site") - .should("be.visible") - .should("have.attr", "href", E2E_EMAIL_REPO_MASTER_LINK) - .should("have.attr", "target", "_blank") - }) - - it('should navigate to the isomer guide on click of the "Get help" button', () => { - // Act - cy.contains("Get help") - .should("be.visible") - .should("have.attr", "href", ISOMER_GUIDE_LINK) - .should("have.attr", "target", "_blank") - }) - - it("should navigate to the settings page when manage site settings is clicked", () => { - // Act - cy.contains("Site settings") - .should("be.visible") - .parent() - .parent() - .siblings("a") - .should( - "have.attr", - "href", - `/sites/${E2E_EMAIL_TEST_SITE.repo}/settings` - ) - .click() - - // Assert - cy.contains("Site settings").should("be.visible") - }) - - it("should navigate to the workspace when edit site is clicked", () => { - // Act - cy.contains("Edit site") - .should("be.visible") - .should( - "have.attr", - "href", - `/sites/${E2E_EMAIL_TEST_SITE.repo}/workspace` - ) - .click() - - // Assert - cy.contains("My Workspace").should("be.visible") - cy.contains(REVIEW_REQUEST_ALERT_MESSAGE).should("not.exist") - }) -}) diff --git a/cypress/e2e/editPage.spec.ts b/cypress/e2e/editPage.spec.ts deleted file mode 100644 index ac563ab4e..000000000 --- a/cypress/e2e/editPage.spec.ts +++ /dev/null @@ -1,446 +0,0 @@ -import "cypress-file-upload" -import { - slugifyCategory, - generateResourceFileName, - titleToPageFileName, -} from "utils/fileNameUtils" - -import { - CMS_BASEURL, - Interceptors, - TEST_REPO_NAME, -} from "../fixtures/constants" -import { SUCCESSFUL_EDIT_PAGE_TOAST } from "../fixtures/messages" - -// Constants -const PRIMARY_COLOUR = "rgb(255, 0, 0)" -const SECONDARY_COLOUR = "rgb(0, 255, 0)" - -describe("editPage.spec", () => { - beforeEach(() => { - cy.setupDefaultInterceptors() - cy.setGithubSessionDefaults() - }) - - describe("Edit unlinked page", () => { - const TEST_PAGE_CONTENT = "lorem ipsum" - const TEST_INSTAGRAM_EMBED_SCRIPT = - '' - const TEST_SANITIZED_INSTAGRAM_EMBED_SCRIPT = - '' - const TEST_UNTRUSTED_SCRIPT = - '' - const TEST_INLINE_SCRIPT = '' - - const TEST_UNLINKED_PAGE_TITLE = "Test Unlinked Page" - const TEST_UNLINKED_PAGE_FILENAME = titleToPageFileName( - TEST_UNLINKED_PAGE_TITLE - ) - const TEST_PAGE_TITLE_ENCODED = encodeURIComponent( - TEST_UNLINKED_PAGE_FILENAME - ) - - const DEFAULT_IMAGE_TITLE = "isomer-logo.svg" - const ADDED_IMAGE_TITLE = "balloon" - const ADDED_IMAGE_PATH = "images/balloon.png" - - const ADDED_FILE_TITLE = "singapore-pages" - const ADDED_FILE_PATH = "files/singapore.pdf" - - const LINK_TITLE = "link" - const LINK_URL = "https://www.google.com" - - before(() => { - cy.setDefaultSettings() - - // NOTE: We need to repeat the interceptor here as - // cypress resolves by type before nesting level. - // Hence, the alias here will not be resolved as the `before` hook - // will be resolved before the outer `beforeEach` - cy.setupDefaultInterceptors() - - // Set up test resource categories - cy.visit(`/sites/${TEST_REPO_NAME}/workspace`) - cy.contains("a", "Create page").click({ force: true }) - cy.get("#title").clear().type(TEST_UNLINKED_PAGE_TITLE) - cy.contains("Save").click().wait(Interceptors.POST) - }) - - beforeEach(() => { - cy.visit(`/sites/${TEST_REPO_NAME}/editPage/${TEST_PAGE_TITLE_ENCODED}`) - cy.contains("verify").should("not.exist") - }) - - it("Edit page (unlinked) should have correct colour", () => { - cy.get("#display-header").should( - "have.css", - "background-color", - PRIMARY_COLOUR - ) - }) - - it("Edit page (unlinked) should have name of title", () => { - cy.contains(TEST_UNLINKED_PAGE_TITLE) - }) - - it("Edit page (unlinked) should provide a warning to users when navigating away", () => { - cy.get(".CodeMirror-scroll").type(TEST_PAGE_CONTENT) - cy.get('button[aria-label="Back to sites"]').click() - - cy.contains("Warning") - cy.contains(":button", "No").click() - - // Sanity check: still in unlinked pages and content still present - cy.url().should( - "include", - `${CMS_BASEURL}/sites/${TEST_REPO_NAME}/editPage/${TEST_PAGE_TITLE_ENCODED}` - ) - cy.contains(TEST_PAGE_CONTENT) - - cy.get('button[aria-label="Back to sites"]').click() - - cy.contains("Warning") - cy.contains(":button", "Yes").click() - - // Assert: in Workspace - cy.url().should( - "include", - `${CMS_BASEURL}/sites/${TEST_REPO_NAME}/workspace` - ) - }) - - it("Edit page (unlinked) should allow user to modify and save content", () => { - cy.get(".CodeMirror-scroll").type(TEST_PAGE_CONTENT) - cy.contains(":button", "Save").click() - - // Asserts - // 1. Toast - cy.contains(SUCCESSFUL_EDIT_PAGE_TOAST) - - // 2. Content is there even after refreshing - cy.reload() - cy.contains(TEST_PAGE_CONTENT).should("exist") - }) - - it("Edit page (unlinked) should allow user to add existing image", () => { - // NOTE: Multiple GET requests are fired off and hence, unable to use default GET to match - cy.intercept("**/images").as("getImages") - cy.get(".image").click().wait("@getImages") - cy.contains(DEFAULT_IMAGE_TITLE).should("exist").click() - cy.contains(":button", "Select").click() - - cy.get("#altText").clear().type("Hello World") - cy.contains(":button", "Save").click() - - cy.contains(`/images/${DEFAULT_IMAGE_TITLE}`) - }) - - it("Edit page (unlinked) should allow user to upload and add new image", () => { - cy.get(".image").click().wait(Interceptors.GET) - cy.contains(":button", "Add new").click() - - cy.get("#file-upload").attachFile(ADDED_IMAGE_PATH) - cy.get("#name").clear().type(ADDED_IMAGE_TITLE) - cy.get("button") - .contains(/^Upload$/) - .click() - .wait(Interceptors.POST) - - cy.get("#altText").clear().type("Hello World") - cy.contains(":button", "Save").click() - - cy.contains(`/images/${ADDED_IMAGE_TITLE}`) - }) - - it("Edit page (unlinked) should allow user to upload and add new file", () => { - cy.get(".file").click().wait(Interceptors.GET) - cy.contains(":button", "Add new").click() - - cy.get("#file-upload").attachFile(ADDED_FILE_PATH) - cy.get("#name").clear().type(ADDED_FILE_TITLE) - cy.get("button") - .contains(/^Upload$/) - .click() - .wait(Interceptors.POST) - - cy.get("#altText").clear().type("Hello World") - cy.contains(":button", "Save").click() - - cy.contains(`/files/${ADDED_FILE_TITLE}`) - }) - - it("Edit page (unlinked) should allow user to add existing file", () => { - cy.get(".file").click().wait(Interceptors.GET) - cy.contains(ADDED_FILE_TITLE).click() - cy.contains(":button", "Select").click() - - cy.get("#altText").clear().type("Hello World") - cy.contains(":button", "Save").click() - cy.contains(`/files/${ADDED_FILE_TITLE}`) - }) - - it("Edit page (unlinked) should allow user to add link", () => { - cy.get(".link").click() - - cy.get('input[id="text"]').type(LINK_TITLE) - cy.get('input[id="link"]').type(LINK_URL) - cy.contains(":button", "Save").click() - - cy.contains(`[${LINK_TITLE}](${LINK_URL})`) - }) - - it("Edit page (unlinked) should allow users to add Instagram embed script", () => { - cy.get(".CodeMirror-scroll").type(TEST_INSTAGRAM_EMBED_SCRIPT) - cy.contains(":button", "Save").click() - - // Asserts - // 1. Toast - cy.contains(SUCCESSFUL_EDIT_PAGE_TOAST) - - // 2. Content is there even after refreshing - cy.reload() - cy.contains(TEST_SANITIZED_INSTAGRAM_EMBED_SCRIPT).should("exist") - }) - - it("Edit page (unlinked) should not allow users to add untrusted external scripts", () => { - cy.get(".CodeMirror-scroll").type(TEST_UNTRUSTED_SCRIPT) - - // Asserts - // 1. Save button is disabled - cy.contains(":button", "Save").should("be.disabled") - - // 2. CSP warning appears - cy.contains( - "Intended ' + const TEST_SANITIZED_INSTAGRAM_EMBED_SCRIPT = + '' + const TEST_UNTRUSTED_SCRIPT = + '' + const TEST_INLINE_SCRIPT = '' + + const TEST_UNLINKED_PAGE_TITLE = "Test Unlinked Page" + const TEST_UNLINKED_PAGE_FILENAME = titleToPageFileName( + TEST_UNLINKED_PAGE_TITLE + ) + const TEST_PAGE_TITLE_ENCODED = encodeURIComponent( + TEST_UNLINKED_PAGE_FILENAME + ) + + const DEFAULT_IMAGE_TITLE = "isomer-logo.svg" + const ADDED_IMAGE_TITLE = "balloon" + const ADDED_IMAGE_PATH = "images/balloon.png" + + const ADDED_FILE_TITLE = "singapore-pages" + const ADDED_FILE_PATH = "files/singapore.pdf" + + const LINK_TITLE = "link" + const LINK_URL = "https://www.google.com" + + const UNLINKED_PAGE = { + frontMatter: { + title: TEST_UNLINKED_PAGE_TITLE, + permalink: "/permalink", + variant: "tiptap", + description: "", + }, + } + + test.beforeEach(async ({ page, context }) => { + setEmailSessionDefaults(context, "Email admin") + const api = await getApi(await context.storageState()) + await api.post("v2/sites/e2e-email-test-repo/pages/pages", { + data: { + content: UNLINKED_PAGE, + newFileName: TEST_UNLINKED_PAGE_FILENAME, + }, + }) + await page.goto( + `/sites/${E2E_EMAIL_TEST_SITE.repo}/editPage/${TEST_PAGE_TITLE_ENCODED}` + ) + }) + + test("Edit page (unlinked) should have correct colour", async ({ + page, + context, + }) => { + // Arrange + const api = await getApi(await context.storageState()) + const header = page.locator("#display-header") + + // Act + await api.post("v2/sites/e2e-email-test-repo/settings", { + data: BASE_SETTINGS, + }) + page.reload() + + // Assert + await expect(header).toHaveCSS("background-color", PRIMARY_COLOUR) + }) + + test("Edit page (unlinked) should have name of title", async ({ page }) => { + // Act + const title = page.getByRole("heading", { + name: TEST_UNLINKED_PAGE_TITLE, + }) + + // Assert + // NOTE: At least 1 heading + await expect(await title.count()).toBeGreaterThan(0) + }) + + test("Edit page (unlinked) should provide a warning to users when navigating away", async ({ + page, + }) => { + // Arrange + await page.getByRole("textbox").fill(TEST_PAGE_CONTENT) + const backButton = page.getByRole("button", { name: "Back to sites" }) + await backButton.click() + + // Act + const warningModal = page.getByText("Warning") + await expect(warningModal).toBeVisible() + await page.getByRole("button", { name: "No" }).click() + + // Sanity check: still in unlinked pages and content still present + await expect(page.url()).toContain( + `${E2E_EMAIL_TEST_SITE.repo}/editPage/${TEST_PAGE_TITLE_ENCODED}` + ) + await expect(page.getByText(TEST_PAGE_CONTENT)).toBeVisible() + + await backButton.click() + await expect(warningModal).toBeVisible() + await page.getByRole("button", { name: "Yes" }).click() + + // Assert + await expect(page.url()).toContain( + `${E2E_EMAIL_TEST_SITE.repo}/workspace` + ) + }) + + test("Edit page (unlinked) should allow user to modify and save content", async ({ + page, + }) => { + // Arrange + await page.getByRole("textbox").fill(TEST_PAGE_CONTENT) + const saveButton = page.getByRole("button", { name: "Save" }) + + // Act + await saveButton.click() + + // Assert + // 1. Toast + await expect(page.getByText(SUCCESSFUL_EDIT_PAGE_TOAST)).toBeVisible() + + // 2. Content is there even after refreshing + await page.reload() + await expect(page.getByText(TEST_PAGE_CONTENT)).toBeVisible() + }) + + test("Edit page (unlinked) should allow user to add existing image", async ({ + page, + }) => { + // Arrange + const addImageButton = page.getByRole("button", { + name: "Insert Image", + }) + await addImageButton.click() + const defaultImageButton = page.getByRole("button").filter({ + hasText: DEFAULT_IMAGE_TITLE, + }) + await defaultImageButton.click() + + // Act + await page.getByRole("button", { name: "Add image to page" }).click() + await page.getByPlaceholder("Alt text").fill("Hello World") + await page.locator("form").getByRole("button", { name: "Save" }).click() + + await expect( + page.getByText(`/images/${DEFAULT_IMAGE_TITLE}`) + ).toBeVisible() + }) + + // TODO: Fix this test so that it does not depend on the previous test to create the dependency. + test("Edit page (unlinked) should allow user to upload and add new image", async ({ + page, + }) => { + // Act + const addImageButton = page.getByRole("button", { + name: "Insert Image", + }) + await addImageButton.click() + const uploadNewImageButton = page.getByRole("button", { + name: "Upload new image", + }) + await uploadNewImageButton.click() + const fileChooserPromise = page.waitForEvent("filechooser") + await page + .getByRole("button", { name: "Choose files or drag and drop" }) + .click() + const fileChooser = await fileChooserPromise + await fileChooser.setFiles(ADDED_IMAGE_PATH) + const fileUploadPromise = page.waitForResponse((res) => + // NOTE: Not using a string/path here cos it gets merged + // as we have provided a `baseURL` (our frontend URL) for playwright. + res.url().includes("media/images/pages") + ) + await page.getByRole("button", { name: "Upload" }).click() + await fileUploadPromise + + // Assert + await expect( + page.getByText("Successfully uploaded 1 image") + ).toBeVisible() + await expect(page.getByText(`/images/${ADDED_IMAGE_TITLE}`)).toBeVisible() + }) + + test("Edit page (unlinked) should allow user to upload and add new file", async ({ + page, + }) => { + // Act + const addFileButton = page.getByRole("button", { + name: "Insert File", + }) + await addFileButton.click() + const uploadNewFileButton = page.getByRole("button", { + name: "Upload new file", + }) + await uploadNewFileButton.click() + const fileChooserPromise = page.waitForEvent("filechooser") + await page + .getByRole("button", { name: "Choose files or drag and drop" }) + .click() + const fileChooser = await fileChooserPromise + await fileChooser.setFiles(ADDED_FILE_PATH) + const fileUploadPromise = page.waitForResponse((res) => + // NOTE: Not using a string/path here cos it gets merged + // as we have provided a `baseURL` (our frontend URL) for playwright. + res.url().includes("media/files/pages") + ) + await page.getByRole("button", { name: "Upload" }).click() + await fileUploadPromise + + // Assert + await expect(page.getByText("Successfully uploaded 1 file")).toBeVisible() + await expect(page.getByText(`/images/${ADDED_FILE_TITLE}`)).toBeVisible() + }) + + // TODO: Fix this test so that it does not depend on the previous test to create the dependency. + test("Edit page (unlinked) should allow user to add existing file", async ({ + page, + }) => { + // NOTE: This test has an implicit dependency on the previous test + // We might wish to squash the two tests together in the future. + // Arrange + const addFileButton = page.getByRole("button", { + name: "Insert File", + }) + await addFileButton.click() + const defaultFileButton = page.getByRole("button").filter({ + hasText: ADDED_FILE_TITLE, + }) + await defaultFileButton.click() + + // Act + await page.getByRole("button", { name: "Add file to page" }).click() + await page.getByPlaceholder("Alt text").fill("Hello World") + await page.locator("form").getByRole("button", { name: "Save" }).click() + + await expect(page.getByText(`/files/${ADDED_FILE_TITLE}`)).toBeVisible() + }) + + test("Edit page (unlinked) should allow user to add link", async ({ + page, + }) => { + // Act + await page.getByRole("button", { name: "Insert Link" }).click() + + await page.getByPlaceholder("Text").fill(LINK_TITLE) + await page.getByPlaceholder("Link").fill(LINK_URL) + + await page.getByRole("button", { name: "Save" }).click() + + // Assert + page.getByText(`[${LINK_TITLE}](${LINK_URL})`) + }) + + test("Edit page (unlinked) should allow users to add Instagram embed script", async ({ + page, + }) => { + // Arrange + await page.getByRole("textbox").fill(TEST_INSTAGRAM_EMBED_SCRIPT) + + // Act + await page.getByRole("button", { name: "Save" }).click() + + // Assert + // 1. Toast + await expect(page.getByText(SUCCESSFUL_EDIT_PAGE_TOAST)).toBeVisible() + + // 2. Content is there even after refreshing + await page.reload() + await expect( + page.getByText(TEST_SANITIZED_INSTAGRAM_EMBED_SCRIPT) + ).toBeVisible() + }) + + // TODO: Add functionality to prevent users from adding + // untrusted external scripts + test.skip("Edit page (unlinked) should not allow users to add untrusted external scripts", async ({ + page, + }) => { + // NOTE: This test will fail at present + // Arrange + const PAGE_VIOLATION_WARNING = + "Intended