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();
+ }
+ });
+});