mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Tabs is better for this yay Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
374 lines
18 KiB
JavaScript
374 lines
18 KiB
JavaScript
// Ported from chibistar/automation/starr/test-courses-{newcourse,archive-final,
|
|
// assessment-archive}.cjs — see ~/Downloads/Courses_Test_Case_Documentation.txt.
|
|
//
|
|
// The source scripts were written as a manually-run, multi-phase live-site
|
|
// session (phase B1 writes a JSON handoff file that phase B2 reads, some
|
|
// checks assume "an earlier interrupted run already did X", row-count
|
|
// assertions assume exactly one pre-existing live course). None of that is
|
|
// safe to rerun unattended in CI, so this rebuilds the same TC-ID coverage
|
|
// as one self-contained, idempotent run: every fixture is created fresh
|
|
// under this run's own [QA-TEST] tag and torn down in afterAll — no
|
|
// cross-script file handoff, no assumptions about what else exists in the DB.
|
|
//
|
|
// Not ported (dropped, not silently — see apps/e2e/README or the PR that
|
|
// introduces this file): TC-CRS-001..009/028/030/042/043/046 (Refresh/Export/
|
|
// column-persistence checks tied to exact live single-row counts or file
|
|
// downloads) and TC-CRS-058 (jump-to-question, minor UI convenience).
|
|
const { test, expect } = require('@playwright/test');
|
|
const { APP_URL, USERS } = require('../../helpers/env');
|
|
const { generateClientSession, buildCookies } = require('../../helpers/session');
|
|
const { cleanupQaTestFixtures } = require('../../helpers/cleanup');
|
|
|
|
async function settle(page, ms = 1200) {
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
await page.waitForTimeout(ms);
|
|
}
|
|
|
|
async function finishWizardFrom(page, startStep = 'rewards') {
|
|
const nextBtn = page.getByRole('button', { name: /^next$/i });
|
|
if (startStep === 'basic') { await nextBtn.click(); await page.waitForTimeout(1000); }
|
|
await nextBtn.click(); await page.waitForTimeout(800); // roadmap -> rewards
|
|
await nextBtn.click(); await page.waitForTimeout(800); // rewards -> review
|
|
await page.getByRole('button', { name: /^finish$/i }).click();
|
|
await page.waitForURL('**/view', { timeout: 15000 }).catch(() => {});
|
|
await settle(page, 1500);
|
|
return page.url().match(/\/admin\/courses\/(\d+)\/view/)?.[1] ?? null;
|
|
}
|
|
|
|
async function quickCreateCourse(page, title, code) {
|
|
await page.goto(`${APP_URL}/admin/courses/add`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1000);
|
|
await page.locator('#title').fill(title);
|
|
await page.locator('#course_code').fill(code);
|
|
await page.getByRole('button', { name: /add objective/i }).click();
|
|
await page.locator('input[placeholder="Objective 1"]').fill('QA automation objective.');
|
|
await page.waitForTimeout(400);
|
|
return finishWizardFrom(page, 'basic');
|
|
}
|
|
|
|
async function archiveCourseByCode(page, code) {
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
const row = page.locator('table tbody tr', { hasText: code });
|
|
await row.locator('button').last().click();
|
|
await page.waitForTimeout(400);
|
|
await page.getByText(/^archive course$/i).click();
|
|
await page.waitForTimeout(800);
|
|
await page.getByRole('button', { name: /^archive$/i }).last().click();
|
|
await settle(page, 1500);
|
|
}
|
|
|
|
const TS = Date.now().toString().slice(-6);
|
|
const QA_TITLE = `[QA-TEST] Courses Automation ${TS}`;
|
|
const QA_CODE = `QA-TEST-${TS}`;
|
|
const HELPER_CODE = `QA-TEST-BULK-${TS}`;
|
|
|
|
test.describe.serial('Courses (admin)', () => {
|
|
test.setTimeout(180_000);
|
|
let courseId;
|
|
let helperCourseId;
|
|
|
|
let adminCookies;
|
|
|
|
test.beforeAll(async () => {
|
|
test.setTimeout(60_000);
|
|
const { refreshToken } = await generateClientSession(USERS.admin.email);
|
|
adminCookies = buildCookies(refreshToken);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
const removed = await cleanupQaTestFixtures();
|
|
console.log('QA-TEST fixtures removed:', removed);
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.context().addCookies(adminCookies);
|
|
});
|
|
|
|
test('TC-CRS-010/012/013/014: New Course wizard blocks submission on missing required fields', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page);
|
|
await page.getByRole('button', { name: /^new course$/i }).click();
|
|
await page.waitForURL('**/admin/courses/add', { timeout: 10000 });
|
|
await settle(page, 1200);
|
|
await expect(page.locator('text=Basic Information')).toBeVisible();
|
|
|
|
const nextBtn = page.getByRole('button', { name: /^next$/i });
|
|
|
|
// All fields blank
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.locator('#title')).toBeVisible();
|
|
await expect(page.locator('p.text-destructive').first()).toBeVisible();
|
|
|
|
// Title only
|
|
await page.locator('#title').fill(QA_TITLE);
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.locator('#title')).toBeVisible();
|
|
|
|
// Title + code, objectives still missing
|
|
await page.locator('#course_code').fill(QA_CODE);
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.locator('#title')).toBeVisible();
|
|
});
|
|
|
|
test('TC-CRS-017: unsaved-changes guard blocks navigating away from a dirty wizard', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses/add`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await page.locator('#title').fill(`${QA_TITLE} (guard check)`);
|
|
await page.getByRole('button', { name: /^cancel$/i }).click();
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('[role="alertdialog"]')).toHaveCount(1);
|
|
await page.getByRole('button', { name: /stay on this page/i }).click();
|
|
});
|
|
|
|
test('TC-CRS-016/018/011: step navigation retains data, achievement is radio-not-checklist, happy-path finish', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses/add`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await page.locator('#title').fill(QA_TITLE);
|
|
await page.locator('#course_code').fill(QA_CODE);
|
|
await page.getByRole('button', { name: /add objective/i }).click();
|
|
await page.locator('input[placeholder="Objective 1"]').fill('QA automation objective.');
|
|
|
|
const nextBtn = page.getByRole('button', { name: /^next$/i });
|
|
await nextBtn.click();
|
|
await page.waitForTimeout(1200);
|
|
await page.getByText(/^basic info$/i).click();
|
|
await page.waitForTimeout(600);
|
|
await expect(page.locator('#title')).toHaveValue(QA_TITLE);
|
|
|
|
await nextBtn.click(); await page.waitForTimeout(800); // roadmap
|
|
await nextBtn.click(); await page.waitForTimeout(800); // -> rewards
|
|
|
|
const wantsBtn = page.getByRole('button', { name: /^yes, add one$/i });
|
|
if (await wantsBtn.count()) await wantsBtn.click();
|
|
await page.waitForTimeout(400);
|
|
|
|
await page.getByRole('button', { name: /new achievement/i }).click();
|
|
await page.waitForTimeout(400);
|
|
await page.locator('#ach_key').fill(`qa_test_ach_${TS}_a`);
|
|
await page.locator('#ach_label').fill('QA Test Achievement A');
|
|
await page.getByRole('button', { name: /^create achievement$/i }).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
await page.getByRole('button', { name: /new achievement/i }).click();
|
|
await page.waitForTimeout(400);
|
|
await page.locator('#ach_key').fill(`qa_test_ach_${TS}_b`);
|
|
await page.locator('#ach_label').fill('QA Test Achievement B');
|
|
await page.getByRole('button', { name: /^create achievement$/i }).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
await expect(page.getByText('QA Test Achievement A')).toHaveCount(0);
|
|
await expect(page.getByText('QA Test Achievement B')).toBeVisible();
|
|
|
|
courseId = await finishWizardFrom(page, 'rewards');
|
|
expect(courseId).toBeTruthy();
|
|
});
|
|
|
|
test('TC-CRS-015: duplicate course_code is blocked', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses/add`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await page.locator('#title').fill(`[QA-TEST] Duplicate Code Check ${TS}`);
|
|
await page.locator('#course_code').fill(QA_CODE); // reuse the same code
|
|
await page.getByRole('button', { name: /add objective/i }).click();
|
|
await page.locator('input[placeholder="Objective 1"]').fill('QA duplicate-code check objective.');
|
|
await page.waitForTimeout(400);
|
|
const dupCourseId = await finishWizardFrom(page, 'basic');
|
|
expect(dupCourseId).toBeNull();
|
|
});
|
|
|
|
test('TC-CRS-039/040/041/044: Columns toggle hide/reshow + sort', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page);
|
|
await page.getByRole('button', { name: /^columns$/i }).click();
|
|
await page.waitForTimeout(400);
|
|
const toggleItems = await page.locator('[role="menuitemcheckbox"]').count();
|
|
expect(toggleItems).toBeGreaterThan(1);
|
|
|
|
await page.locator('[role="menuitemcheckbox"]', { hasText: 'Level' }).click();
|
|
await page.waitForTimeout(400);
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.locator('th', { hasText: 'Level' })).toHaveCount(0);
|
|
|
|
await page.getByRole('button', { name: /^columns$/i }).click();
|
|
await page.waitForTimeout(300);
|
|
await page.locator('[role="menuitemcheckbox"]', { hasText: 'Level' }).click();
|
|
await page.waitForTimeout(400);
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.locator('th', { hasText: 'Level' })).toHaveCount(1);
|
|
|
|
const titleHeader = page.locator('th', { hasText: /^Title$/ }).first();
|
|
await titleHeader.locator('button').first().click();
|
|
await page.waitForTimeout(200);
|
|
await page.getByText(/^sort asc$/i).click();
|
|
await page.waitForTimeout(600);
|
|
await expect(titleHeader.locator('svg')).toHaveCount(1, { timeout: 3000 }).catch(() => {});
|
|
});
|
|
|
|
test('TC-CRS-045/047/048/049/050: View Info renders details, 404 for bad id, Edit navigation', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page);
|
|
const row = page.locator('table tbody tr', { hasText: QA_CODE });
|
|
await row.locator('button').last().click();
|
|
await page.waitForTimeout(300);
|
|
await page.getByText(/^view info$/i).click();
|
|
await page.waitForURL('**/view', { timeout: 10000 });
|
|
await settle(page, 1200);
|
|
await expect(page.getByText('Basic Information')).toBeVisible();
|
|
|
|
await page.goto(`${APP_URL}/admin/courses/999999999/view`, { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(3000);
|
|
await expect(page.getByText(/course not found/i)).toBeVisible();
|
|
|
|
await page.goto(`${APP_URL}/admin/courses/${courseId}/view`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await page.getByRole('button', { name: /edit course/i }).first().click();
|
|
await page.waitForURL('**/edit', { timeout: 8000 });
|
|
expect(page.url()).toContain('/edit');
|
|
});
|
|
|
|
test('TC-CRS-055/056/057/053/054/059/060: assessment builder validation', async ({ page }) => {
|
|
await page.goto(`${APP_URL}/admin/courses/${courseId}/assessment`, { waitUntil: 'networkidle' });
|
|
await settle(page, 2500);
|
|
|
|
await page.getByRole('button', { name: /^multi select$/i }).click();
|
|
await page.waitForTimeout(600);
|
|
await page.getByRole('button', { name: /^save assessment$/i }).click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.getByText(/question text is required/i)).toBeVisible();
|
|
|
|
const card = page.locator('.rounded-xl.border.bg-card').last();
|
|
await card.locator('textarea[placeholder*="question here" i]').fill('QA automation multi-select test question.');
|
|
const optionInputs = card.locator('input[placeholder^="Option "]');
|
|
const optCount = await optionInputs.count();
|
|
for (let i = 0; i < optCount; i++) await optionInputs.nth(i).fill(`QA option ${i + 1}`);
|
|
let incorrectToggles = card.locator('button[title="Mark as incorrect"]');
|
|
while (await incorrectToggles.count() > 0) {
|
|
await incorrectToggles.first().click();
|
|
await page.waitForTimeout(200);
|
|
}
|
|
await page.getByRole('button', { name: /^save assessment$/i }).click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.getByText(/at least one correct answer is required/i)).toBeVisible();
|
|
|
|
await card.locator('button[title="Mark as correct"]').first().click();
|
|
await page.waitForTimeout(200);
|
|
await optionInputs.nth(optCount - 1).fill('');
|
|
await page.getByRole('button', { name: /^save assessment$/i }).click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.getByText(/all option texts are required/i)).toBeVisible();
|
|
|
|
await optionInputs.nth(optCount - 1).fill(`QA option ${optCount}`);
|
|
await page.getByRole('button', { name: /^save assessment$/i }).click();
|
|
await page.waitForTimeout(1500);
|
|
await expect(page.locator('p.text-destructive')).toHaveCount(0);
|
|
|
|
const dupStatus = await page.evaluate(async (id) => {
|
|
const res = await fetch(`/api/admin/courses/${id}/assessment`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include', body: JSON.stringify({ title: 'Duplicate attempt' }),
|
|
});
|
|
return res.status;
|
|
}, courseId);
|
|
expect(dupStatus).toBe(409);
|
|
|
|
await page.goto(`${APP_URL}/admin/courses/${courseId}/assessment`, { waitUntil: 'networkidle' });
|
|
await settle(page, 2000);
|
|
const timeLimitVal = await page.locator('input[placeholder="No limit"]').inputValue().catch(() => null);
|
|
expect(timeLimitVal === '' || timeLimitVal === null).toBe(true);
|
|
|
|
await page.locator('input[placeholder="Course Assessment"]').fill(`[QA-TEST] Assessment dirty-${TS}`).catch(() => {});
|
|
await page.waitForTimeout(400);
|
|
await page.goBack();
|
|
await page.waitForTimeout(1000);
|
|
await expect(page.locator('[role="alertdialog"]')).toHaveCount(1);
|
|
});
|
|
|
|
test('TC-CRS-029/031/032/033/034/035/036/037/038: archive / restore / bulk-restore / delete lifecycle', async ({ page }) => {
|
|
helperCourseId = await quickCreateCourse(page, '[QA-TEST] Bulk Restore Helper', HELPER_CODE);
|
|
expect(helperCourseId).toBeTruthy();
|
|
|
|
await archiveCourseByCode(page, QA_CODE);
|
|
await archiveCourseByCode(page, HELPER_CODE);
|
|
|
|
await page.goto(`${APP_URL}/admin/courses/archived`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1500);
|
|
const archivedRows = page.locator('table tbody tr');
|
|
await expect(archivedRows).not.toHaveCount(0);
|
|
await expect(page.getByText(QA_CODE)).toBeVisible();
|
|
await expect(page.getByText(HELPER_CODE)).toBeVisible();
|
|
|
|
// Kebab on an archived row: exactly Restore + Delete
|
|
await archivedRows.first().locator('button').last().click();
|
|
await page.waitForTimeout(400);
|
|
const menuItems = await page.locator('[role="menuitem"]').allInnerTexts();
|
|
expect(menuItems.length).toBe(2);
|
|
expect(menuItems.some((t) => /restore/i.test(t))).toBe(true);
|
|
expect(menuItems.some((t) => /delete/i.test(t))).toBe(true);
|
|
await page.keyboard.press('Escape');
|
|
|
|
// Cancel a restore — row count unchanged
|
|
const beforeCancel = await archivedRows.count();
|
|
await archivedRows.first().locator('button').last().click();
|
|
await page.waitForTimeout(400);
|
|
await page.getByText(/^restore$/i).click();
|
|
await page.waitForTimeout(600);
|
|
await page.getByRole('button', { name: /^cancel$/i }).click();
|
|
await settle(page, 1200);
|
|
await expect(archivedRows).toHaveCount(beforeCancel);
|
|
|
|
// Restore one, confirm active
|
|
await archivedRows.first().locator('button').last().click();
|
|
await page.waitForTimeout(400);
|
|
await page.getByText(/^restore$/i).click();
|
|
await page.waitForTimeout(600);
|
|
await page.getByRole('button', { name: /^restore$/i }).last().click();
|
|
await settle(page, 1800);
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
const activeQaCountAfterSingleRestore = await page.locator('table tbody tr', { hasText: /QA-TEST/i }).count();
|
|
expect(activeQaCountAfterSingleRestore).toBeGreaterThanOrEqual(1);
|
|
|
|
// Re-archive it, then bulk-restore both
|
|
const reArchivedCode = (await page.locator('table tbody tr', { hasText: /QA-TEST/i }).first().innerText()).includes(QA_CODE) ? QA_CODE : HELPER_CODE;
|
|
await archiveCourseByCode(page, reArchivedCode);
|
|
await page.goto(`${APP_URL}/admin/courses/archived`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1500);
|
|
const checkboxes = page.locator('table tbody tr button[role="checkbox"]');
|
|
const cbCount = await checkboxes.count();
|
|
for (let i = 0; i < cbCount; i++) { await checkboxes.nth(i).click(); await page.waitForTimeout(150); }
|
|
await page.getByRole('button', { name: /^restore$/i }).first().click();
|
|
await page.waitForTimeout(600);
|
|
await page.getByRole('button', { name: /^restore/i }).last().click();
|
|
await settle(page, 1800);
|
|
await page.goto(`${APP_URL}/admin/courses`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await expect(page.getByText(QA_CODE)).toBeVisible();
|
|
await expect(page.getByText(HELPER_CODE)).toBeVisible();
|
|
|
|
// Archive both again, then permanently delete each
|
|
await archiveCourseByCode(page, QA_CODE);
|
|
await archiveCourseByCode(page, HELPER_CODE);
|
|
await page.goto(`${APP_URL}/admin/courses/archived`, { waitUntil: 'networkidle' });
|
|
await settle(page, 1500);
|
|
let remaining = await page.locator('table tbody tr', { hasText: /QA-TEST/i }).count();
|
|
expect(remaining).toBeGreaterThanOrEqual(2);
|
|
while (remaining > 0) {
|
|
const row = page.locator('table tbody tr', { hasText: /QA-TEST/i }).first();
|
|
await row.locator('button').last().click();
|
|
await page.waitForTimeout(400);
|
|
await page.getByText(/^delete$/i).click();
|
|
await page.waitForTimeout(800);
|
|
await expect(page.locator('[role="alertdialog"]')).toHaveCount(1);
|
|
await page.getByRole('button', { name: /delete.*permanently/i }).click();
|
|
await settle(page, 1500);
|
|
remaining = await page.locator('table tbody tr', { hasText: /QA-TEST/i }).count();
|
|
}
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
await settle(page, 1200);
|
|
await expect(page.locator('table tbody tr', { hasText: /QA-TEST/i })).toHaveCount(0);
|
|
});
|
|
});
|