- Remove `playwright.config.js` to clean up configuration. - Add new E2E test suites (`admin-pos-orders`, `admin-module-goals`, `admin-module-pos-mobile-order-flow`) to structure Admin module tests. - Introduce reusable authentication and data seeding utilities in `fixtures` for streamlined test flows and maintainability.
318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|
import { bookingTestData, loginAsOperator } from './fixtures';
|
|
|
|
type GoalProgressBucket = {
|
|
count: number;
|
|
target: number;
|
|
};
|
|
|
|
type MockGoal = {
|
|
id: number;
|
|
created_by: number;
|
|
departments: number[];
|
|
criteria: {
|
|
label: string;
|
|
type: 'REVENUE' | 'PRODUCT' | 'VISITS';
|
|
target: number;
|
|
start: string;
|
|
end: string;
|
|
products: number[];
|
|
users: number[];
|
|
departments: number[];
|
|
progress_alert_frequency: 'NONE' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'CHANGED';
|
|
progress_alert_destination: 'NONE' | 'SLACK' | 'EMAIL' | 'SMS';
|
|
progress_alert_progress_type: string;
|
|
progress_alert_style: string;
|
|
progress_alert_format: string | null;
|
|
progress_alert_weekdays: string[];
|
|
progress_alert_time_of_day: string | null;
|
|
department_daily_targets: Record<string, number>;
|
|
};
|
|
progress: {
|
|
all: GoalProgressBucket;
|
|
today: GoalProgressBucket;
|
|
week: GoalProgressBucket;
|
|
month: GoalProgressBucket;
|
|
departmental_distribution: Record<string, Record<'all' | 'today' | 'week' | 'month', GoalProgressBucket>>;
|
|
};
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
const makeMockGoal = (params: {
|
|
id: number;
|
|
departmentId: number;
|
|
label: string;
|
|
target: number;
|
|
start: string;
|
|
end: string;
|
|
allCount?: number;
|
|
todayCount?: number;
|
|
weekCount?: number;
|
|
monthCount?: number;
|
|
}): MockGoal => {
|
|
const allCount = params.allCount ?? 0;
|
|
const todayCount = params.todayCount ?? 0;
|
|
const weekCount = params.weekCount ?? allCount;
|
|
const monthCount = params.monthCount ?? allCount;
|
|
|
|
return {
|
|
id: params.id,
|
|
created_by: 11,
|
|
departments: [params.departmentId],
|
|
criteria: {
|
|
label: params.label,
|
|
type: 'REVENUE',
|
|
target: params.target,
|
|
start: params.start,
|
|
end: params.end,
|
|
products: [],
|
|
users: [],
|
|
departments: [params.departmentId],
|
|
progress_alert_frequency: 'NONE',
|
|
progress_alert_destination: 'NONE',
|
|
progress_alert_progress_type: 'ALL',
|
|
progress_alert_style: 'NONE',
|
|
progress_alert_format: null,
|
|
progress_alert_weekdays: [],
|
|
progress_alert_time_of_day: null,
|
|
department_daily_targets: {},
|
|
},
|
|
progress: {
|
|
all: { count: allCount, target: params.target },
|
|
today: { count: todayCount, target: params.target },
|
|
week: { count: weekCount, target: params.target },
|
|
month: { count: monthCount, target: params.target },
|
|
departmental_distribution: {
|
|
[String(params.departmentId)]: {
|
|
all: { count: allCount, target: params.target },
|
|
today: { count: todayCount, target: params.target },
|
|
week: { count: weekCount, target: params.target },
|
|
month: { count: monthCount, target: params.target },
|
|
},
|
|
},
|
|
},
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
};
|
|
|
|
const setupGoalsApiMock = async (page: Page, departmentId: number) => {
|
|
let goals: MockGoal[] = [
|
|
makeMockGoal({
|
|
id: 1,
|
|
departmentId,
|
|
label: 'Mock Active Goal',
|
|
target: 100,
|
|
allCount: 45,
|
|
todayCount: 10,
|
|
weekCount: 28,
|
|
monthCount: 45,
|
|
start: '2026-01-01',
|
|
end: '2099-01-01',
|
|
}),
|
|
makeMockGoal({
|
|
id: 2,
|
|
departmentId,
|
|
label: 'Mock Expired Goal',
|
|
target: 200,
|
|
allCount: 180,
|
|
todayCount: 0,
|
|
weekCount: 0,
|
|
monthCount: 0,
|
|
start: '2024-01-01',
|
|
end: '2024-02-01',
|
|
}),
|
|
];
|
|
|
|
await page.route('**/goals/department/progress-alert/test**', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: { destination: 'EMAIL' } }),
|
|
});
|
|
});
|
|
|
|
await page.route('**/goals/department**', async (route) => {
|
|
const request = route.request();
|
|
const method = request.method();
|
|
|
|
if (method === 'GET') {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: goals }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (method === 'PUT') {
|
|
const body = request.postDataJSON() as { id: number; departments: number[]; criteria: Record<string, unknown> };
|
|
const id = Number(body.id);
|
|
const departments = (body.departments || [departmentId]).map((x) => Number(x));
|
|
|
|
goals = goals.map((goal) => {
|
|
if (goal.id !== id) return goal;
|
|
return {
|
|
...goal,
|
|
departments,
|
|
criteria: {
|
|
...goal.criteria,
|
|
...(body.criteria || {}),
|
|
departments,
|
|
},
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
});
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: goals.find((g) => g.id === id) || null }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (method === 'DELETE') {
|
|
const id = Number(new URL(request.url()).searchParams.get('id'));
|
|
goals = goals.filter((goal) => goal.id !== id);
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: { success: true } }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ data: {} }),
|
|
});
|
|
});
|
|
};
|
|
|
|
test.describe('Admin Module - Goals', () => {
|
|
const getGoalsUrl = (departmentId: string) => `/admin/${departmentId}/modules/goals`;
|
|
|
|
const openGoalAction = async (page: Page, label: string, actionLabel: string) => {
|
|
const card = page.locator('.goal-card', { hasText: label }).first();
|
|
await expect(card).toBeVisible({ timeout: 15000 });
|
|
|
|
const dropdown = card.locator('.dropdown.is-hoverable').first();
|
|
await dropdown.hover();
|
|
await card.locator('.dropdown-item', { hasText: actionLabel }).click({ force: true });
|
|
};
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name.toLowerCase().includes('mobile'), 'Goals admin suite is desktop-focused');
|
|
test.skip(testInfo.project.name !== 'chromium', 'Goals admin suite is stabilized for Chromium in this environment.');
|
|
|
|
await loginAsOperator(page);
|
|
|
|
const departmentId = bookingTestData.departmentId;
|
|
await setupGoalsApiMock(page, departmentId);
|
|
|
|
await page.goto(getGoalsUrl(String(departmentId)));
|
|
await expect(page).toHaveURL(new RegExp(`/admin/${departmentId}/modules/goals`));
|
|
await expect(page.locator('nav.level.box')).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test('loads goals page and supports status/timeframe tab switching', async ({ page }) => {
|
|
const statusTabs = page.locator('.tabs.is-toggle').first();
|
|
const timeframeTabs = page.locator('.tabs.is-toggle').nth(1);
|
|
|
|
await expect(page.locator('.goal-card')).toHaveCount(1);
|
|
|
|
await statusTabs.locator('li').nth(1).click();
|
|
await expect(statusTabs.locator('li').nth(1)).toHaveClass(/is-active/);
|
|
await expect(page.locator('.goal-card')).toHaveCount(1);
|
|
|
|
await statusTabs.locator('li').nth(2).click();
|
|
await expect(statusTabs.locator('li').nth(2)).toHaveClass(/is-active/);
|
|
await expect(page.locator('.goal-card')).toHaveCount(2);
|
|
|
|
await statusTabs.locator('li').nth(0).click();
|
|
await expect(statusTabs.locator('li').nth(0)).toHaveClass(/is-active/);
|
|
|
|
await timeframeTabs.locator('li', { hasText: 'Indtil nu' }).click();
|
|
await expect(timeframeTabs.locator('li', { hasText: 'Indtil nu' })).toHaveClass(/is-active/);
|
|
|
|
await timeframeTabs.locator('li').nth(2).click();
|
|
await expect(timeframeTabs.locator('li').nth(2)).toHaveClass(/is-active/);
|
|
|
|
await timeframeTabs.locator('li').nth(3).click();
|
|
await expect(timeframeTabs.locator('li').nth(3)).toHaveClass(/is-active/);
|
|
|
|
await timeframeTabs.locator('li').nth(4).click();
|
|
await expect(timeframeTabs.locator('li').nth(4)).toHaveClass(/is-active/);
|
|
|
|
await timeframeTabs.locator('li').nth(0).click();
|
|
await expect(timeframeTabs.locator('li').nth(0)).toHaveClass(/is-active/);
|
|
});
|
|
|
|
test('opens create goal modal and supports cancel', async ({ page }) => {
|
|
await page.locator('button:has(.fa-plus), button:has-text("Opret")').first().click();
|
|
|
|
const modal = page.locator('.swal2-popup');
|
|
await expect(modal).toBeVisible({ timeout: 15000 });
|
|
|
|
await expect(modal.locator('input[placeholder*="f.eks."]').first()).toBeVisible();
|
|
await expect(modal.locator('input[type="number"]').first()).toBeVisible();
|
|
await expect(modal.locator('input[type="date"]').first()).toBeVisible();
|
|
await expect(modal.locator('textarea').first()).toBeVisible();
|
|
|
|
await modal.locator('button.button:has-text("Annuller")').click();
|
|
await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 });
|
|
});
|
|
|
|
test('submits edit modal and sends expected update payload', async ({ page }) => {
|
|
await openGoalAction(page, 'Mock Active Goal', 'Rediger');
|
|
|
|
const modal = page.locator('.swal2-popup');
|
|
await expect(modal).toBeVisible({ timeout: 15000 });
|
|
|
|
await modal.locator('input[placeholder*="f.eks."]').first().fill('Mock Active Goal Updated');
|
|
await modal.locator('input[type="number"]').first().fill('321');
|
|
|
|
const putRequestPromise = page.waitForRequest((request) =>
|
|
request.method() === 'PUT' && request.url().includes('/goals/department')
|
|
);
|
|
|
|
await modal.locator('.buttons.is-right .button.is-dark').click({ force: true });
|
|
|
|
const putRequest = await putRequestPromise;
|
|
const putPayload = putRequest.postDataJSON() as { criteria?: { label?: string; target?: number } };
|
|
|
|
expect(putPayload.criteria?.label).toBe('Mock Active Goal Updated');
|
|
expect(Number(putPayload.criteria?.target)).toBe(321);
|
|
|
|
const successDialog = page.locator('.swal2-popup:has-text("Succes")').first();
|
|
await expect(successDialog).toBeVisible({ timeout: 15000 });
|
|
await expect(successDialog).toBeHidden({ timeout: 15000 });
|
|
});
|
|
|
|
test('supports test-alert action and delete request flow', async ({ page }) => {
|
|
await openGoalAction(page, 'Mock Active Goal', 'Test alert');
|
|
|
|
const toast = page.locator('.v-toast__item').first();
|
|
await expect(toast).toBeVisible({ timeout: 10000 });
|
|
await expect(toast).toContainText(/Test alert sendt via|Kunne ikke sende test alert/);
|
|
|
|
await openGoalAction(page, 'Mock Active Goal', 'Slet');
|
|
await expect(page.locator('.swal2-popup')).toBeVisible({ timeout: 10000 });
|
|
|
|
const deleteRequestPromise = page.waitForRequest((request) =>
|
|
request.method() === 'DELETE' && request.url().includes('/goals/department')
|
|
);
|
|
|
|
await page.locator('.swal2-popup .swal2-confirm').click();
|
|
|
|
const deleteRequest = await deleteRequestPromise;
|
|
expect(deleteRequest.url()).toContain('id=');
|
|
await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 });
|
|
});
|
|
});
|
|
|