diff --git a/README.md b/README.md index a56d6e8b..e692b507 100644 --- a/README.md +++ b/README.md @@ -367,13 +367,18 @@ Playwright is used to test different functionalities of the content system. Befo ``` JIRA_REPORTER_ID= FLASK_DEBUG=1 +DISABLE_SSO=1 ``` **Note**: -Replace `` with a valid reporter ID from JIra. This reporter will be used when creating Jira tasks as a result of running some tests. +Replace `` with a valid reporter ID from JIra. This reporter will be used when creating Jira tasks as a result of running some tests. -Replace `` with the ID of Canonical provided VPN which will be used to access Directory API for fetching users in different tests. +Update the following in `tests/config.ts` + +``` +BASE_URL: `http://localhost:${process.env.PORT}` +``` #### Running Playwright tests diff --git a/static/client/components/OwnerAndReviewers/Owner.tsx b/static/client/components/OwnerAndReviewers/Owner.tsx index 8c18e868..e8c4134e 100644 --- a/static/client/components/OwnerAndReviewers/Owner.tsx +++ b/static/client/components/OwnerAndReviewers/Owner.tsx @@ -7,6 +7,7 @@ import CustomSearchAndFilter from "@/components/Common/CustomSearchAndFilter"; import IconTextWithTooltip from "@/components/Common/IconTextWithTooltip"; import config from "@/config"; import { PagesServices } from "@/services/api/services/pages"; +import { getDefaultUser } from "@/services/api/services/users"; import { type IUser } from "@/services/api/types/users"; const Owner = ({ page, onSelectOwner }: IOwnerAndReviewersProps): JSX.Element => { @@ -14,12 +15,18 @@ const Owner = ({ page, onSelectOwner }: IOwnerAndReviewersProps): JSX.Element => const { options, setOptions, handleChange } = useUsersRequest(); useEffect(() => { + let owner = null; + if (Boolean(window.__E2E_TESTING__)) { + owner = getDefaultUser(); + } + if (page) { - let owner = page.owner as IUser | null; + owner = page.owner as IUser | null; if (page.owner.name === "Default" || !page.owner.email) owner = null; - setCurrentOwner(owner); - if (onSelectOwner) onSelectOwner(owner); } + + setCurrentOwner(owner); + if (onSelectOwner) onSelectOwner(owner); }, [onSelectOwner, page]); const handleRemoveOwner = useCallback( diff --git a/static/client/components/RequestTaskModal/RequestTaskModal.tsx b/static/client/components/RequestTaskModal/RequestTaskModal.tsx index 9b2cc53b..8da3305e 100644 --- a/static/client/components/RequestTaskModal/RequestTaskModal.tsx +++ b/static/client/components/RequestTaskModal/RequestTaskModal.tsx @@ -257,7 +257,8 @@ const RequestTaskModal = ({ )} {((webpage.status !== PageStatus.NEW && changeType === ChangeRequestType.PAGE_REMOVAL) || - changeType !== ChangeRequestType.PAGE_REMOVAL) && ( + changeType !== ChangeRequestType.PAGE_REMOVAL || + window.__E2E_TESTING__) && ( )} diff --git a/static/client/config/index.ts b/static/client/config/index.ts index 71c3b6a0..9cbb80c4 100644 --- a/static/client/config/index.ts +++ b/static/client/config/index.ts @@ -8,7 +8,8 @@ export const VIEW_TREE = "tree"; export const VIEW_TABLE = "table"; const config = { - projects: ["canonical.com", "ubuntu.com", "cn.ubuntu.com", "jp.ubuntu.com"], + allProjects: ["canonical.com", "ubuntu.com", "cn.ubuntu.com", "jp.ubuntu.com"], + testProjects: ["canonical.com", "jp.ubuntu.com"], views: [VIEW_OWNED, VIEW_REVIEWED, VIEW_TREE, VIEW_TABLE] as TView[], tooltips: { ownerDef: "Owners request the page and must approve the page for it to go live.", diff --git a/static/client/global.d.ts b/static/client/global.d.ts new file mode 100644 index 00000000..e906769c --- /dev/null +++ b/static/client/global.d.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface Window { + __E2E_TESTING__?: boolean; + } +} diff --git a/static/client/services/api/hooks/pages.ts b/static/client/services/api/hooks/pages.ts index f7c0782d..af0967b5 100644 --- a/static/client/services/api/hooks/pages.ts +++ b/static/client/services/api/hooks/pages.ts @@ -8,8 +8,9 @@ import type { IPagesResponse } from "@/services/api/types/pages"; import type { IApiBasicError, IUseQueryHookRest } from "@/services/api/types/query"; export function usePages(noCache: boolean = false): IUseQueryHookRest { + let projects = window.__E2E_TESTING__ ? config.testProjects : config.allProjects; const results = useQueries[]>( - config.projects.map((project) => { + projects.map((project) => { return { queryKey: ["pages", project], queryFn: () => PagesServices.getPages(project, noCache).then((response) => response.data), diff --git a/tests/config.ts b/tests/config.ts index 06d5f5d5..b03e5f65 100644 --- a/tests/config.ts +++ b/tests/config.ts @@ -1,5 +1,4 @@ export const config = { BASE_URL: `http://localhost`, - PLAYWRIGHT_TEST_USER: "Muhammad Ali", // TODO: replace with a dedicated test user account name from IS PLAYWRIGHT_TEST_PAGE_URL: "playwright-test-page", }; diff --git a/tests/index.spec.ts b/tests/index.spec.ts index 941b9c7a..82abb4a0 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test"; import { config } from "./config"; +import { selectTreeView } from "./utils/common"; test.describe("Test Application Layout", () => { test("displays the login page", async ({ page }) => { @@ -13,10 +14,25 @@ test.describe("Test Application Layout", () => { test("projects are loaded and visible", async ({ page }) => { await page.goto(`${config.BASE_URL}/app`); - const tree = await page.locator(".l-navigation__drawer .p-panel__content .p-list-tree").first(); - await expect(tree).toBeVisible(); - const children = await tree.locator(".p-list-tree__item"); - expect(await children.count()).toBeGreaterThan(0); + await selectTreeView(page); + + // check projects are present + const projectsDropdown = page.locator("select.l-site-selector"); + const projects = projectsDropdown.locator("option"); + const projectCount = await projects.count(); + expect(projectCount).toBeGreaterThan(0); + + // check all projects have pages + for (let i = 0; i < projectCount; i++) { + const value = await projects.nth(i).getAttribute("value"); + if (value) { + await projectsDropdown.selectOption(value); + const tree = page.locator(".l-navigation__drawer .p-panel__content .p-list-tree").first(); + const pages = tree.locator(".p-list-tree__item"); + const pageCount = await pages.count(); + expect(pageCount).toBeGreaterThan(0); + } + } }); }); diff --git a/tests/project.spec.ts b/tests/project.spec.ts index 5d077fd9..b7758336 100644 --- a/tests/project.spec.ts +++ b/tests/project.spec.ts @@ -1,6 +1,7 @@ import { test, expect, APIRequestContext } from "@playwright/test"; import { config } from "./config"; import type { IJiraTask } from "@/services/api/types/pages"; +import { removeWebpage, selectTreeView } from "./utils/common"; const JIRA_TASKS: IJiraTask[] = []; let apiContext: APIRequestContext; @@ -13,45 +14,29 @@ test.describe("Test project actions", () => { }); test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.__E2E_TESTING__ = true; + }); + await page.setExtraHTTPHeaders({ "X-JIRA-REPORTER-ID": process.env.JIRA_REPORTER_ID || "", }); await page.goto(`${config.BASE_URL}/app`); + + // select tree view + await page.locator(".l-navigation__drawer .p-panel__content .p-side-navigation__link").nth(1).click(); }); test("remove page", async ({ page }) => { + await selectTreeView(page); const tree = page.locator(".l-navigation__drawer .p-panel__content .p-list-tree").first(); const child = tree.locator(".p-list-tree__item").first(); await child.click(); - await expect(page.getByRole("heading", { name: /Title/i })).toBeVisible(); - page.getByRole("button", { name: /Request removal/i }).click(); - const modal = page.locator(".p-modal").first(); - await expect(modal).toBeVisible(); - await expect(page.getByRole("heading", { name: /Submit request for page removal/i })).toBeVisible(); - await modal.locator('input[type="date"]').fill(new Date().toISOString().split("T")[0]); - const checkboxes = await page.locator("input[type='checkbox'][required]"); - if (checkboxes) { - const checkboxCount = await checkboxes.count(); - for (let i = 0; i < checkboxCount; i++) { - await checkboxes.nth(i).check(); - } - } - - const responsePromise = page.waitForResponse((response) => response.url().includes("request-removal")); - await modal.getByRole("button", { name: /Submit/i }).click(); - const response = await responsePromise; - - if (response.status() === 200) { - const responseBody = await response.json(); - if (responseBody.jira_task_id) { - JIRA_TASKS.push(responseBody.jira_task_id); - } - } - - await expect(page.locator(".l-notification__container .p-notification--negative")).not.toBeVisible(); + await removeWebpage(page, JIRA_TASKS); }); test("request page changes", async ({ page }) => { + await selectTreeView(page); const tree = page.locator(".l-navigation__drawer .p-panel__content .p-list-tree").first(); const child = tree.locator(".p-list-tree__item").first(); await child.click(); @@ -84,6 +69,7 @@ test.describe("Test project actions", () => { }); test("create new page", async ({ page }) => { + await selectTreeView(page); await page.getByRole("button", { name: /Request new page/i }).click(); await expect(page.getByRole("heading", { name: /New page/i })).toBeVisible(); await page.locator("input[aria-labelledby='url-title']").fill(config.PLAYWRIGHT_TEST_PAGE_URL); @@ -131,6 +117,8 @@ test.describe("Test project actions", () => { } await expect(page.locator(".l-notification__container .p-notification--negative")).not.toBeVisible(); + await page.waitForTimeout(5000); + await removeWebpage(page, JIRA_TASKS); }); test.afterAll(async () => { diff --git a/tests/utils/common.ts b/tests/utils/common.ts new file mode 100644 index 00000000..18a898f5 --- /dev/null +++ b/tests/utils/common.ts @@ -0,0 +1,47 @@ +import { Page, expect } from "@playwright/test"; + +export async function removeWebpage(page: Page, JIRA_TASKS: string[]): Promise { + page.getByRole("button", { name: /Request removal/i }).click(); + const modal = page.locator(".p-modal").first(); + await expect(modal).toBeVisible(); + await expect(page.getByRole("heading", { name: /Submit request for page removal/i })).toBeVisible(); + await modal.locator('input[type="date"]').fill(new Date().toISOString().split("T")[0]); + const checkboxes = await page.locator("input[type='checkbox'][required]"); + if (checkboxes) { + const checkboxCount = await checkboxes.count(); + for (let i = 0; i < checkboxCount; i++) { + await checkboxes.nth(i).check(); + } + } + + const responsePromise = page.waitForResponse((response) => response.url().includes("request-removal")); + await modal.getByRole("button", { name: /Submit/i }).click(); + const response = await responsePromise; + + if (response.status() === 200) { + const responseBody = await response.json(); + if (responseBody.jira_task_id) { + JIRA_TASKS.push(responseBody.jira_task_id); + } + } + + await expect(page.locator(".l-notification__container .p-notification--negative")).not.toBeVisible(); +} + +export async function selectTableView(page: Page): Promise { + const tableViewListItem = page.locator(".l-navigation__drawer .p-panel__content .p-side-navigation__link", { + hasText: /Table view/i, + }); + await tableViewListItem.click(); + + await page.getByText("/Loading projects. Please wait./i").waitFor({ state: "detached" }); +} + +export async function selectTreeView(page: Page): Promise { + const treeViewListItem = page.locator(".l-navigation__drawer .p-panel__content .p-side-navigation__link", { + hasText: /Tree view/i, + }); + await treeViewListItem.click(); + + await page.getByText("/Loading.../i").waitFor({ state: "detached" }); +} diff --git a/tests/views/table-view.spec.ts b/tests/views/table-view.spec.ts new file mode 100644 index 00000000..14a2b68b --- /dev/null +++ b/tests/views/table-view.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "@playwright/test"; +import { config } from "../config"; +import { selectTableView } from "../utils/common"; + +test.beforeEach(async ({ page }) => { + await page.goto(`${config.BASE_URL}/app`); +}); + +test.describe("Test Table View", () => { + test("table view is visible", async ({ page }) => { + await selectTableView(page); + // expect a page showing all the projects in an accordion + expect(page.getByRole("heading", { name: /All pages/i })).toBeVisible(); + const projects = page.locator(".p-accordion__list .p-accordion__group"); + const projectCount = await projects.count(); + expect(projectCount).toBeGreaterThan(0); + + // check all projects have pages + for (let i = 0; i < projectCount; i++) { + const project = projects.nth(i); + const projectHeading = project.locator(".p-accordion__heading"); + const projectPageCount = await projectHeading.locator(".p-badge").innerText(); + expect(parseInt(projectPageCount)).toBeGreaterThan(1); + + // select each project + await project.click(); + + // select a random page + const pages = project.locator(".p-accordion__panel table tbody tr"); + const pagesCount = await pages.count(); + const selectedPage = project + .locator(".p-accordion__panel table tbody tr") + .nth(Math.floor(Math.random() * pagesCount)); + await selectedPage.locator(".p-button--link").first().click(); + + // check the page details are visible + await expect(page.getByText(/Description/i).first()).toBeVisible(); + + // click the back button + await page.getByRole("button", { name: /Back/i }).click(); + } + }); +});