diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 28123917..39620ee8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -15,13 +15,17 @@ jobs: node-version: lts/* - 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@v4 - if: ${{ !cancelled() }} + + - name: Upload failed screenshots + if: failure() + uses: actions/upload-artifact@v4 with: - name: playwright-report - path: playwright-report/ + name: failed-snapshots + path: test-results/ retention-days: 30 diff --git a/playwright.config.ts b/playwright.config.ts index bdd81f64..8032b60c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -47,23 +47,19 @@ export default defineConfig({ use: { ...devices["Desktop Safari"] }, }, - /* - TODO: re-enable when we understand how to direct tests for mobile (for instance some element worth checking on desktop might be hidden on mobile and visa versa). { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, + name: "Mobile Safari", + use: { ...devices["iPhone 12"] }, }, - { - name: 'Mobile Safari', - use: { ...devices['iPhone 14'] }, + name: "Mobile Safari", + use: { ...devices["iPhone 14"] }, }, { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, }, - */ ], webServer: { diff --git a/tests/e2e/NewOrgModal.spec.ts b/tests/e2e/NewOrgModal.spec.ts index b1a30aaa..a9f9e968 100644 --- a/tests/e2e/NewOrgModal.spec.ts +++ b/tests/e2e/NewOrgModal.spec.ts @@ -11,13 +11,22 @@ test.describe("without being signed in", () => { await API.getAccount(page, "NOT_SIGNED_IN"); }); - test("can *NOT* see the 'New Org' button", async ({ page }) => { + test("can *NOT* see the 'New Org' button", async ({ page, isMobile }) => { const response = await page.goto("http://localhost:1234"); expect(response?.status()).toBeLessThan(400); + await expect(page).toHaveScreenshot("no-new-org-button.png", { + maxDiffPixels: 100, + }); - await expect( - page.locator(".signed-in-nav_desktop .button").getByText("New Org"), - ).not.toBeVisible(); + if (isMobile) { + await expect( + page.locator(".signed-in-nav_mobile .button").getByText("New Org"), + ).not.toBeVisible(); + } else { + await expect( + page.locator(".signed-in-nav_desktop .button").getByText("New Org"), + ).not.toBeVisible(); + } }); }); @@ -26,12 +35,21 @@ test.describe("while being signed in", () => { await API.getAccount(page, "@alice", { isSuperadmin: true }); }); - test("can see the 'New Org' button", async ({ page }) => { + test("can see the 'New Org' button", async ({ page, isMobile }) => { const response = await page.goto("http://localhost:1234"); expect(response?.status()).toBeLessThan(400); + await expect(page).toHaveScreenshot("new-org-button.png", { + maxDiffPixels: 100, + }); - await expect( - page.locator(".signed-in-nav_desktop .button").getByText("New Org"), - ).toBeVisible(); + if (isMobile) { + await expect( + page.locator(".signed-in-nav_mobile .button").getByText("New Org"), + ).toBeVisible(); + } else { + await expect( + page.locator(".signed-in-nav_desktop .button").getByText("New Org"), + ).toBeVisible(); + } }); }); diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Chrome-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Chrome-darwin.png new file mode 100644 index 00000000..332de738 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Chrome-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Safari-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Safari-darwin.png new file mode 100644 index 00000000..115fe954 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-Mobile-Safari-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-chromium-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-chromium-darwin.png new file mode 100644 index 00000000..76f936d6 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-chromium-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-firefox-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-firefox-darwin.png new file mode 100644 index 00000000..924d3681 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-firefox-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-webkit-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-webkit-darwin.png new file mode 100644 index 00000000..3160bcc9 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/new-org-button-webkit-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Chrome-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Chrome-darwin.png new file mode 100644 index 00000000..c7e6d313 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Chrome-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Safari-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Safari-darwin.png new file mode 100644 index 00000000..a78629e4 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-Mobile-Safari-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-chromium-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-chromium-darwin.png new file mode 100644 index 00000000..0993cc51 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-chromium-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-firefox-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-firefox-darwin.png new file mode 100644 index 00000000..1ab6c1b9 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-firefox-darwin.png differ diff --git a/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-webkit-darwin.png b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-webkit-darwin.png new file mode 100644 index 00000000..b479e090 Binary files /dev/null and b/tests/e2e/NewOrgModal.spec.ts-snapshots/no-new-org-button-webkit-darwin.png differ diff --git a/tests/e2e/OrgPeoplePage.spec.ts b/tests/e2e/OrgPeoplePage.spec.ts index 75761ffa..38d14e75 100644 --- a/tests/e2e/OrgPeoplePage.spec.ts +++ b/tests/e2e/OrgPeoplePage.spec.ts @@ -57,7 +57,7 @@ test.describe("with org:manage permission", () => { await API.getAccount(page, "@alice"); }); - test("can view the org people page", async ({ page }) => { + test("can view the org people page", async ({ page, isMobile }) => { const orgHandle = "@unison"; await API.getOrgProfile(page, orgHandle, { permissions: ["org:manage"] }); await API.getOrgRoleAssignments(page, orgHandle); @@ -73,7 +73,11 @@ test.describe("with org:manage permission", () => { const projects = page.locator(".page-content .profile-snippet"); await expect(projects).toHaveCount(3); - await expect(navItem(page, "People")).toBeVisible(); + // The nav is hidden on mobile + if (!isMobile) { + await expect(navItem(page, "People")).toBeVisible(); + } + await expect(button(page, "Add an organization member")).toBeVisible(); }); }); diff --git a/tests/e2e/ProjectOverviewPage.spec.ts b/tests/e2e/ProjectOverviewPage.spec.ts index e0b29fe6..1f1bd242 100644 --- a/tests/e2e/ProjectOverviewPage.spec.ts +++ b/tests/e2e/ProjectOverviewPage.spec.ts @@ -14,7 +14,7 @@ test.describe("without being signed in", () => { await API.getAccount(page, "NOT_SIGNED_IN"); }); - test("can view public project", async ({ page }) => { + test("can view public project", async ({ page, isMobile }) => { await API.getProject(page, "@unison/base"); const response = await page.goto("http://localhost:1234/@unison/base"); @@ -25,13 +25,17 @@ test.describe("without being signed in", () => { await expect(button(page, "Browse Project Code")).toBeVisible(); await expect(button(page, "Edit summary")).not.toBeVisible(); - await expect(navItem(page, "Code")).toBeVisible(); - await expect(navItem(page, "Tickets")).toBeVisible(); - await expect(navItem(page, "Contributions")).toBeVisible(); - await expect(navItem(page, "Settings")).not.toBeVisible(); + + // Nav is hidden on mobile + if (!isMobile) { + await expect(navItem(page, "Code")).toBeVisible(); + await expect(navItem(page, "Tickets")).toBeVisible(); + await expect(navItem(page, "Contributions")).toBeVisible(); + await expect(navItem(page, "Settings")).not.toBeVisible(); + } }); - test("can *not* view a private project`", async ({ page }) => { + test("can *not* view a private project`", async ({ page, isMobile }) => { await API.getProject_(page, "@bob/private-project", { status: 404 }); const response = await page.goto( @@ -43,10 +47,14 @@ test.describe("without being signed in", () => { await expect( page.getByText("Couldn't find @bob/private-project"), ).toBeVisible(); - await expect(navItem(page, "Code")).not.toBeVisible(); - await expect(navItem(page, "Tickets")).not.toBeVisible(); - await expect(navItem(page, "Contributions")).not.toBeVisible(); - await expect(navItem(page, "Settings")).not.toBeVisible(); + + // Nav is hidden on mobile + if (!isMobile) { + await expect(navItem(page, "Code")).not.toBeVisible(); + await expect(navItem(page, "Tickets")).not.toBeVisible(); + await expect(navItem(page, "Contributions")).not.toBeVisible(); + await expect(navItem(page, "Settings")).not.toBeVisible(); + } }); }); @@ -60,6 +68,7 @@ test.describe("while signed in", () => { test.describe("with an another user's private project and `project:view` permission", () => { test("can view, but *not* edit summary or see settings", async ({ page, + isMobile, }) => { await API.getProject(page, "@bob/private-project", { visibility: "private", @@ -71,17 +80,21 @@ test.describe("while signed in", () => { ); expect(response?.status()).toBeLessThan(400); - await expect(button(page, "Edit summary")).not.toBeVisible(); - await expect(navItem(page, "Code")).toBeVisible(); - await expect(navItem(page, "Tickets")).toBeVisible(); - await expect(navItem(page, "Contributions")).toBeVisible(); - await expect(navItem(page, "Settings")).not.toBeVisible(); + // Nav is hidden on mobile + if (!isMobile) { + await expect(button(page, "Edit summary")).not.toBeVisible(); + await expect(navItem(page, "Code")).toBeVisible(); + await expect(navItem(page, "Tickets")).toBeVisible(); + await expect(navItem(page, "Contributions")).toBeVisible(); + await expect(navItem(page, "Settings")).not.toBeVisible(); + } }); }); test.describe("with an another user's private project and `project:maintain` permission", () => { test("can view and edit summary, but not see settings", async ({ page, + isMobile, }) => { await API.getProject(page, "@bob/private-project", { visibility: "private", @@ -93,18 +106,21 @@ test.describe("while signed in", () => { ); expect(response?.status()).toBeLessThan(400); - // await expect(button(page, "Browse Project Code")).toBeVisible(); - await expect(button(page, "Edit summary")).toBeVisible(); - await expect(navItem(page, "Code")).toBeVisible(); - await expect(navItem(page, "Tickets")).toBeVisible(); - await expect(navItem(page, "Contributions")).toBeVisible(); - await expect(navItem(page, "Settings")).not.toBeVisible(); + // Nav is hidden on mobile + if (!isMobile) { + await expect(button(page, "Edit summary")).toBeVisible(); + await expect(navItem(page, "Code")).toBeVisible(); + await expect(navItem(page, "Tickets")).toBeVisible(); + await expect(navItem(page, "Contributions")).toBeVisible(); + await expect(navItem(page, "Settings")).not.toBeVisible(); + } }); }); test.describe("with an another user's private project and `project:manage` permission", () => { test("can view and edit summary, but not see settings", async ({ page, + isMobile, }) => { await API.getProject(page, "@bob/private-project", { visibility: "private", @@ -116,12 +132,14 @@ test.describe("while signed in", () => { ); expect(response?.status()).toBeLessThan(400); - // await expect(button(page, "Browse Project Code")).toBeVisible(); - await expect(button(page, "Edit summary")).toBeVisible(); - await expect(navItem(page, "Code")).toBeVisible(); - await expect(navItem(page, "Tickets")).toBeVisible(); - await expect(navItem(page, "Contributions")).toBeVisible(); - await expect(navItem(page, "Settings")).toBeVisible(); + // Nav is hidden on mobile + if (!isMobile) { + await expect(button(page, "Edit summary")).toBeVisible(); + await expect(navItem(page, "Code")).toBeVisible(); + await expect(navItem(page, "Tickets")).toBeVisible(); + await expect(navItem(page, "Contributions")).toBeVisible(); + await expect(navItem(page, "Settings")).toBeVisible(); + } }); }); }); diff --git a/tests/e2e/TestHelpers/Data.ts b/tests/e2e/TestHelpers/Data.ts index 139f3122..b606b5f1 100644 --- a/tests/e2e/TestHelpers/Data.ts +++ b/tests/e2e/TestHelpers/Data.ts @@ -1,5 +1,8 @@ import { faker } from "@faker-js/faker"; +// Ensures test data is the same between runs (especially useful for screenshot comparisons) +faker.seed(42); + function account(handle: string) { return { ...user(), @@ -21,7 +24,7 @@ function user(handle?: string) { const handle_ = handle ? handle : userHandle(); return { - avatarUrl: faker.image.avatar(), + avatarUrl: faker.image.personPortrait(), handle: handle_.replace("@", ""), name: `${firstName} ${lastName}`, userId: faker.string.uuid(),