Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions apps/dash/e2e/auth/login.spec.ts

This file was deleted.

54 changes: 54 additions & 0 deletions apps/dash/e2e/features/auth/login.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect, test } from "@playwright/test";
import { iHaveLoggedInAsSuperAdmin } from "../../helper/auth";
import { LoginPage } from "../../pages/auth/LoginPage";

test.describe("Authentication - Login Flow", () => {
let loginPage: LoginPage;

test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});

test("should login successfully with valid super admin credentials", async ({
page,
context,
}) => {
// This uses the helper which internally performs the login steps
// We are refactoring the original test to include better verification
await iHaveLoggedInAsSuperAdmin(context);
await page.goto("/");

// Verification: Not on login page anymore
await expect(page).not.toHaveURL("/login");
// Verification: Top navbar should be visible (indicator of dashboard access)
const topNavbar = page.getByRole("navigation").first();
await expect(topNavbar).toBeVisible();
});

test("should show error for invalid credentials", async () => {
await loginPage.login("wrong@example.com", "wrongpassword");

// The login form is expected to show an error message
const errorMsg = await loginPage.getErrorMessage();
expect(errorMsg).toBeTruthy();
await expect(loginPage.errorMessage).toContainText(
/Invalid email or password|Email atau kata sandi tidak valid/i,
);
});

test("should show error for missing mandatory fields", async ({ page }) => {
// Clicking login without filling anything triggers HTML5 validation
// or specific form error if we prevent default
await loginPage.loginButton.click();

// Check if HTML5 validation message appears (browser-level)
// Or if we have a UI-driven error instead.
// In our LoginForm, email/password are 'required'
const emailField = page.locator("#login-email");
const isInvalid = await emailField.evaluate(
(node: HTMLInputElement) => !node.checkValidity(),
);
expect(isInvalid).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { AccountStatus } from "@domus/core";
import { expect, test } from "@playwright/test";
import { iHaveLoggedInAs } from "../helper";
import { iHaveLoggedInAs } from "../../helper";
import { LoginPage } from "../../pages/auth/LoginPage";
import { PendingPage } from "../../pages/auth/PendingPage";
import { RejectedPage } from "../../pages/auth/RejectedPage";

test.describe("Authentication Redirects (AUTH-04)", () => {
test("unauthenticated user should be redirected to login", async ({
page,
}) => {
const loginPage = new LoginPage(page);
await page.goto("/");
await expect(page).toHaveURL(/\/login/);
await expect(page.locator("text=Masuk dengan Google")).toBeVisible();
await expect(loginPage.googleButton).toBeVisible();
});

test("approved user should be able to access the dashboard", async ({
Expand All @@ -35,6 +39,7 @@ test.describe("Authentication Redirects (AUTH-04)", () => {
page,
context,
}) => {
const pendingPage = new PendingPage(page);
await iHaveLoggedInAs(context, {
email: "pending@example.com",
name: "Pending User",
Expand All @@ -43,13 +48,14 @@ test.describe("Authentication Redirects (AUTH-04)", () => {

await page.goto("/");
await expect(page).toHaveURL(/\/pending/);
await expect(page.locator("text=Akun Sedang Ditinjau")).toBeVisible();
await expect(pendingPage.statusTitle).toBeVisible();
});

test("rejected user should be redirected to /rejected", async ({
page,
context,
}) => {
const rejectedPage = new RejectedPage(page);
await iHaveLoggedInAs(context, {
email: "rejected@example.com",
name: "Rejected User",
Expand All @@ -58,7 +64,39 @@ test.describe("Authentication Redirects (AUTH-04)", () => {

await page.goto("/");
await expect(page).toHaveURL(/\/rejected/);
await expect(page.locator("text=Akses Ditolak")).toBeVisible();
await expect(rejectedPage.statusTitle).toBeVisible();
});

test("pending users should be redirected to /pending from deep links", async ({
page,
context,
}) => {
const pendingPage = new PendingPage(page);

await iHaveLoggedInAs(context, {
email: "pending-deep@example.com",
name: "Pending Deep User",
accountStatus: AccountStatus.Pending,
});
await page.goto("/organizations");
await expect(page).toHaveURL(/\/pending/);
await expect(pendingPage.statusTitle).toBeVisible();
});

test("rejected users should be redirected to /rejected from deep links", async ({
page,
context,
}) => {
const rejectedPage = new RejectedPage(page);

await iHaveLoggedInAs(context, {
email: "rejected-deep@example.com",
name: "Rejected Deep User",
accountStatus: AccountStatus.Rejected,
});
await page.goto("/organizations");
await expect(page).toHaveURL(/\/rejected/);
await expect(rejectedPage.statusTitle).toBeVisible();
});

test("authenticated approved user should be redirected away from /login", async ({
Expand Down
31 changes: 14 additions & 17 deletions apps/dash/e2e/features/org/create.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { expect, test } from "@playwright/test";
import { iHaveLoggedInAsSuperAdmin } from "../../helper/auth";
import { OrgCreatePage } from "../../pages/org/OrgCreatePage";

test.describe("Organization Create Page", () => {
test.beforeEach(async ({ context }) => {
let createPage: OrgCreatePage;

test.beforeEach(async ({ page, context }) => {
createPage = new OrgCreatePage(page);
// 1. Setup session via auth helper
await iHaveLoggedInAsSuperAdmin(context);
});
Expand All @@ -11,15 +15,12 @@ test.describe("Organization Create Page", () => {
page,
}) => {
// 1. Navigate to /org/new
await page.goto("/org/new");
await createPage.goto();

// 2. Verify page header and description
await expect(page.getByTestId("org-new-title")).toBeVisible();

// Specifically target the paragraph in the header to avoid ambiguity with textarea
await expect(createPage.title).toBeVisible();
await expect(
page
.locator("header p, div.text-center p")
createPage.description
.filter({
hasText:
/Lengkapi detail informasi untuk mendaftarkan struktur organisasi baru/i,
Expand All @@ -31,19 +32,15 @@ test.describe("Organization Create Page", () => {
await expect(page.getByText(/Informasi Dasar/i)).toBeVisible();

// 4. Verify input fields and fill data
const nameInput = page.locator("#name");
await expect(nameInput).toBeVisible();
await nameInput.fill("Organisasi Baru PKRBT");
await expect(nameInput).toHaveValue("Organisasi Baru PKRBT");
await expect(createPage.nameInput).toBeVisible();
await createPage.fillName("Organisasi Baru PKRBT");
await expect(createPage.nameInput).toHaveValue("Organisasi Baru PKRBT");

// 5. Verify submit button and handle submission redirect
const submitButton = page.getByRole("button", {
name: /Simpan/i,
});
await expect(submitButton).toBeVisible();
await expect(createPage.submitButton).toBeVisible();

// 6. Click submit and verify redirection to /org
await submitButton.click();
// 6. Click submit and verify redirection to /org/[:id]
await createPage.submit();
await expect(page).toHaveURL(/\/(dash|)?org\/.*/);
});
});
124 changes: 41 additions & 83 deletions apps/dash/e2e/features/org/hierarchy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { expect, test } from "@playwright/test";
import { iHaveLoggedInAsSuperAdmin } from "../../helper/auth";
import { OrgCreatePage } from "../../pages/org/OrgCreatePage";

test.describe("Organization Hierarchy Rules (E2E)", () => {
let createPage: OrgCreatePage;

test.beforeEach(async ({ page, context }) => {
createPage = new OrgCreatePage(page);
// Log browser console to terminal for debugging
page.on("console", (msg) => {
if (msg.type() === "log") console.log(`BROWSER LOG: ${msg.text()}`);
Expand All @@ -12,34 +16,19 @@ test.describe("Organization Hierarchy Rules (E2E)", () => {
await iHaveLoggedInAsSuperAdmin(context);
});

test("should enforce hierarchy filtering in creation form", async ({
page,
}) => {
test("should enforce hierarchy filtering in creation form", async () => {
// 1. Navigate to /org/new
await page.goto("/org/new");

const typeSelect = page.getByTestId("org-type-select");
const parentSelect = page.getByTestId("org-parent-select");
await createPage.goto();

// Helper to select organization type
const selectType = async (
// Helper to select organization type and verify selection
const selectAndVerifyType = async (
typeName: string | RegExp,
technicalValue: string,
) => {
// 1. Click to open
await typeSelect.click({ force: true });
const listbox = page.getByRole("listbox");
await expect(listbox).toBeVisible({ timeout: 5000 });

// 2. Select the option
const option = listbox.getByRole("option", { name: typeName });
await option.click({ force: true });
await createPage.selectType(typeName);

// 3. Wait for listbox to close
await expect(listbox).not.toBeVisible({ timeout: 5000 });

// 4. Verify selection text (Indonesian label OR technical value)
await expect(typeSelect).toHaveText(
// Verify selection text (Indonesian label OR technical value)
await expect(createPage.typeSelect).toHaveText(
new RegExp(
`(${typeName instanceof RegExp ? typeName.source : typeName}|${technicalValue})`,
"i",
Expand All @@ -48,89 +37,58 @@ test.describe("Organization Hierarchy Rules (E2E)", () => {
);
};

const waitForParentToLoad = async () => {
await expect(parentSelect).toHaveAttribute("data-loading", "false", {
timeout: 15000,
});
};

// Case 1: Region should disable parent selection
await selectType(/Wilayah/i, "region");
await expect(parentSelect).toBeDisabled();
// Use a relaxed text check for the disabled state text/placeholder
await expect(parentSelect).toContainText(/tidak memiliki induk/i);
await selectAndVerifyType(/Wilayah|Region/i, "region");
await expect(createPage.parentSelect).toBeDisabled();
await expect(createPage.parentSelect).toContainText(
/tidak memiliki induk/i,
);

// Case 2: Station should allow parent selection
await selectType(/Stasi/i, "station");
await expect(parentSelect).toBeEnabled();
await waitForParentToLoad();
await selectAndVerifyType(/Stasi|Station/i, "station");
await expect(createPage.parentSelect).toBeEnabled();
await createPage.waitForParentToLoad();

// Verify it allows opening
await parentSelect.click();
await createPage.parentSelect.click();
await expect(
page.getByRole("option", { name: /Tanpa Induk/i }),
createPage.page.getByRole("option", { name: /Tanpa Induk/i }),
).toBeVisible();
await page.keyboard.press("Escape");
await createPage.page.keyboard.press("Escape");

// Case 3: BEC should be enabled
await selectType(/Lingkungan/i, "bec");
await expect(parentSelect).toBeEnabled();
await waitForParentToLoad();
await selectAndVerifyType(/Lingkungan|Environment/i, "bec");
await expect(createPage.parentSelect).toBeEnabled();
await createPage.waitForParentToLoad();

// Case 4: Categorical should be enabled
await selectType(/Kategorial/i, "categorical");
await expect(parentSelect).toBeEnabled();
await waitForParentToLoad();
await selectAndVerifyType(/Kategorial|Categorical/i, "categorical");
await expect(createPage.parentSelect).toBeEnabled();
await createPage.waitForParentToLoad();
});

test("should clear invalid parent when type changes", async ({ page }) => {
await page.goto("/org/new");

const typeSelect = page.getByTestId("org-type-select");
const parentSelect = page.getByTestId("org-parent-select");

const selectType = async (
typeName: string | RegExp,
technicalValue: string,
) => {
await typeSelect.click({ force: true });
const listbox = page.getByRole("listbox");
await expect(listbox).toBeVisible();
await listbox
.getByRole("option", { name: typeName })
.click({ force: true });
await expect(typeSelect).toHaveText(
new RegExp(
`(${typeName instanceof RegExp ? typeName.source : typeName}|${technicalValue})`,
"i",
),
{ timeout: 10000 },
);
};

const waitForParentToLoad = async () => {
await expect(parentSelect).toHaveAttribute("data-loading", "false", {
timeout: 15000,
});
};
test("should clear invalid parent when type changes", async () => {
await createPage.goto();

// 1. Set type to BEC
await selectType(/Lingkungan/i, "bec");
await expect(parentSelect).toBeEnabled();
await waitForParentToLoad();
await createPage.selectType(/Lingkungan|Environment/i);
await expect(createPage.parentSelect).toBeEnabled();
await createPage.waitForParentToLoad();

// 2. Select any parent
await parentSelect.click();
const firstOption = page.getByRole("option").first();
await createPage.parentSelect.click();
const firstOption = createPage.page.getByRole("option").first();
await expect(firstOption).toBeVisible();
await firstOption.click();
await waitForParentToLoad();
await createPage.waitForParentToLoad();

// 3. Change type to Region
await selectType(/Wilayah/i, "region");
await createPage.selectType(/Wilayah|Region/i);

// 4. Verify parent was cleared and disabled
await expect(parentSelect).toBeDisabled();
await expect(parentSelect).toContainText(/tidak memiliki induk/i);
await expect(createPage.parentSelect).toBeDisabled();
await expect(createPage.parentSelect).toContainText(
/tidak memiliki induk/i,
);
});
});
Loading