diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 696637fa..21c26c80 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -80,6 +80,11 @@ jobs: --network host \ websites-content-system & sleep 3 curl --head --fail --retry-delay 1 --retry 50 --retry-connrefused http://localhost + + - name: Load repositories + run: | + curl http://localhost/app & sleep 8 + - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run Playwright tests diff --git a/entrypoint b/entrypoint index df89abbd..881533ac 100755 --- a/entrypoint +++ b/entrypoint @@ -23,6 +23,17 @@ activate() { RUN_COMMAND="gunicorn webapp.app:app --name $(hostname) --workers=2 --bind $1" + # Start Celery worker if Redis is available + if [ ! -z ${REDIS_HOST+x} ]; then + CELERY_COMMAND="celery -A webapp.app.celery_app worker -B --loglevel=" + if [ ! -z ${FLASK_DEBUG+x} ]; then + CELERY_COMMAND="${CELERY_COMMAND}debug" + else + CELERY_COMMAND="${CELERY_COMMAND}info" + fi + ${CELERY_COMMAND} & + fi + if [ -z ${FLASK_DEBUG+x} ]; then RUN_COMMAND="${RUN_COMMAND} --reload --log-level debug --timeout 9999" fi 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 394fdbaa..6a02e66f 100644 --- a/static/client/components/RequestTaskModal/RequestTaskModal.tsx +++ b/static/client/components/RequestTaskModal/RequestTaskModal.tsx @@ -260,7 +260,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..e1c02a97 100644 --- a/static/client/config/index.ts +++ b/static/client/config/index.ts @@ -7,8 +7,10 @@ export const VIEW_REVIEWED = "reviewed"; export const VIEW_TREE = "tree"; export const VIEW_TABLE = "table"; +export const projects = ["canonical.com", "ubuntu.com", "cn.ubuntu.com", "jp.ubuntu.com"]; + const config = { - projects: ["canonical.com", "ubuntu.com", "cn.ubuntu.com", "jp.ubuntu.com"], + projects: window.__E2E_TESTING__ ? projects.slice(-2) : projects, 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/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(); + } + }); +});