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