7a2ae8434d
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true
154 lines
5.5 KiB
JavaScript
154 lines
5.5 KiB
JavaScript
// E2E:
|
|
// 1) An app with an `image_url` renders that image (not a Lucide icon)
|
|
// on the launcher tile.
|
|
// 2) An app with `open_mode='external_tab'` opens its `external_url` in
|
|
// a new browser tab when the launcher tile is clicked (we intercept
|
|
// window.open).
|
|
// 3) An app with `open_mode='external_iframe'` navigates the SPA to
|
|
// `/embedded/:id`, which renders an <iframe src=externalUrl>.
|
|
// All three apps are inserted directly into the DB (via pg) and cleaned
|
|
// up in afterAll, so this test is self-contained.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
import pg from "pg";
|
|
|
|
const DATABASE_URL = process.env.DATABASE_URL;
|
|
if (!DATABASE_URL) {
|
|
throw new Error("DATABASE_URL must be set to run this test");
|
|
}
|
|
|
|
const TEST_PASSWORD = "TestPass123!";
|
|
const TEST_PASSWORD_HASH =
|
|
"$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";
|
|
|
|
// External https URL — resolveServiceImageUrl passes http(s) through
|
|
// untouched, so the <img src=...> on the tile equals this exact value.
|
|
// We don't actually need the network fetch to succeed; we only assert
|
|
// that an <img> with this src is rendered (i.e. a custom image is
|
|
// being used instead of a Lucide icon).
|
|
const CUSTOM_IMAGE_URL = "https://example.invalid/test-tile-image.png";
|
|
|
|
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
|
|
|
const createdAppIds = [];
|
|
const createdUserIds = [];
|
|
let username;
|
|
|
|
function uniqueSuffix() {
|
|
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
}
|
|
|
|
async function insertApp({ slug, nameEn, openMode, imageUrl, externalUrl, route }) {
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO apps
|
|
(slug, name_ar, name_en, icon_name, route, color, sort_order,
|
|
is_active, image_url, external_url, open_mode)
|
|
VALUES ($1, $2, $3, 'Grid2X2', $4, '#6366f1', 9999,
|
|
TRUE, $5, $6, $7)
|
|
RETURNING id`,
|
|
[slug, nameEn, nameEn, route, imageUrl, externalUrl, openMode],
|
|
);
|
|
const id = rows[0].id;
|
|
createdAppIds.push(id);
|
|
return id;
|
|
}
|
|
|
|
test.afterAll(async () => {
|
|
if (createdAppIds.length > 0) {
|
|
await pool.query(`DELETE FROM apps WHERE id = ANY($1::int[])`, [createdAppIds]);
|
|
}
|
|
if (createdUserIds.length > 0) {
|
|
await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds]);
|
|
await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [createdUserIds]);
|
|
}
|
|
await pool.end();
|
|
});
|
|
|
|
test.beforeAll(async () => {
|
|
username = `e2e_app_open_${uniqueSuffix()}`;
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
|
|
VALUES ($1, $2, $3, $1, 'en', TRUE) RETURNING id`,
|
|
[username, `${username}@example.com`, TEST_PASSWORD_HASH],
|
|
);
|
|
createdUserIds.push(rows[0].id);
|
|
await pool.query(
|
|
`INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'user'`,
|
|
[rows[0].id],
|
|
);
|
|
});
|
|
|
|
async function loginUI(page) {
|
|
await page.goto("/login");
|
|
await page.locator("#username").fill(username);
|
|
await page.locator("#password").fill(TEST_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15_000 }),
|
|
page.locator('form button[type="submit"]').click(),
|
|
]);
|
|
}
|
|
|
|
test("launcher renders custom image_url and external open modes work", async ({ page, context }) => {
|
|
// Insert three apps with distinct, identifiable names so we can find
|
|
// their tiles on the launcher.
|
|
const stamp = uniqueSuffix();
|
|
const imageAppName = `ImageApp_${stamp}`;
|
|
const tabAppName = `TabApp_${stamp}`;
|
|
const iframeAppName = `IframeApp_${stamp}`;
|
|
|
|
await insertApp({
|
|
slug: `image_app_${stamp}`,
|
|
nameEn: imageAppName,
|
|
openMode: "internal",
|
|
imageUrl: CUSTOM_IMAGE_URL,
|
|
externalUrl: null,
|
|
route: `/img-${stamp}`,
|
|
});
|
|
await insertApp({
|
|
slug: `tab_app_${stamp}`,
|
|
nameEn: tabAppName,
|
|
openMode: "external_tab",
|
|
imageUrl: null,
|
|
externalUrl: "https://example.com/tab-target",
|
|
route: "/unused-tab",
|
|
});
|
|
const iframeAppId = await insertApp({
|
|
slug: `iframe_app_${stamp}`,
|
|
nameEn: iframeAppName,
|
|
openMode: "external_iframe",
|
|
imageUrl: null,
|
|
externalUrl: "https://example.com/iframe-target",
|
|
route: "/unused-iframe",
|
|
});
|
|
|
|
await loginUI(page);
|
|
|
|
// 1) Custom image rendering on launcher tile.
|
|
const imageTile = page.getByText(imageAppName, { exact: true }).first();
|
|
await expect(imageTile).toBeVisible({ timeout: 15_000 });
|
|
// Tile should contain an <img> whose src is our custom image URL —
|
|
// proves the launcher uses the uploaded image instead of a Lucide icon.
|
|
const tileImg = page.locator(`img[src="${CUSTOM_IMAGE_URL}"]`).first();
|
|
await expect(tileImg).toHaveCount(1);
|
|
|
|
// 2) external_tab opens external URL in a new tab.
|
|
// window.open is what the launcher uses for external_tab. Stub it
|
|
// to record the URL instead of actually navigating.
|
|
await page.evaluate(() => {
|
|
window.__opened = [];
|
|
window.open = (url) => {
|
|
window.__opened.push(String(url));
|
|
return null;
|
|
};
|
|
});
|
|
await page.getByText(tabAppName, { exact: true }).first().click();
|
|
const openedUrls = await page.evaluate(() => window.__opened ?? []);
|
|
expect(openedUrls).toContain("https://example.com/tab-target");
|
|
|
|
// 3) external_iframe → SPA navigates to /embedded/:id and renders iframe.
|
|
await page.getByText(iframeAppName, { exact: true }).first().click();
|
|
await page.waitForURL(new RegExp(`/embedded/${iframeAppId}(?:$|[?/])`));
|
|
const iframeEl = page.locator(`iframe[src="https://example.com/iframe-target"]`);
|
|
await expect(iframeEl).toBeVisible({ timeout: 10_000 });
|
|
});
|