Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac731ae19 | ||
|
|
2f2beb774a | ||
|
|
a98dc061da |
@@ -1,6 +1,16 @@
|
||||
FROM node:24-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
ARG RELEASE_COMMIT_SHA=""
|
||||
ARG COMMIT_SHA=""
|
||||
ARG GITHUB_SHA=""
|
||||
ARG SOURCE_COMMIT=""
|
||||
ARG VITE_BASE_PATH=""
|
||||
ENV RELEASE_COMMIT_SHA="${RELEASE_COMMIT_SHA}"
|
||||
ENV COMMIT_SHA="${COMMIT_SHA}"
|
||||
ENV GITHUB_SHA="${GITHUB_SHA}"
|
||||
ENV SOURCE_COMMIT="${SOURCE_COMMIT}"
|
||||
ENV VITE_BASE_PATH="${VITE_BASE_PATH}"
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : 3,
|
||||
workers: isCI ? 2 : 1,
|
||||
reporter: [["list"], ["html", { open: "never", outputFolder: "output/playwright/prod/report" }]],
|
||||
outputDir: "output/playwright/prod/test-results",
|
||||
use: {
|
||||
|
||||
@@ -240,7 +240,7 @@ const getProductOptionsLabel = (vehicle) => {
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="false"
|
||||
:displayActionsDirectly="true"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
|
||||
+2
-2
@@ -197,7 +197,7 @@ onMounted(() => {
|
||||
<div class="error-state__content">
|
||||
<p>{{ t('superuser_invoice_distribution.errors.overview_load_failed') }}: {{ errorMessage }}</p>
|
||||
<b-button type="is-danger" class="control-button is-light" @click="loadOverview">
|
||||
{{ t('common.retry') }}
|
||||
{{ t('superuser_invoice_distribution.actions.try_again') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-message>
|
||||
@@ -281,7 +281,7 @@ onMounted(() => {
|
||||
<div v-if="!sortedMonthSummaries.length" class="empty-state has-text-centered">
|
||||
<p class="has-text-grey mb-3">{{ t('superuser_invoice_distribution.empty.no_months') }}</p>
|
||||
<b-button type="is-link" class="control-button is-light" @click="loadOverview">
|
||||
{{ t('common.retry') }}
|
||||
{{ t('superuser_invoice_distribution.actions.try_again') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function attachPageHealthGuards(page: Page, baseURL: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function expectBodyHasContent(page: Page, minimumLength = 20) {
|
||||
export async function expectBodyHasContent(page: Page, minimumLength = 20, timeout = 45_000) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
@@ -101,7 +101,7 @@ export async function expectBodyHasContent(page: Page, minimumLength = 20) {
|
||||
.catch(() => "");
|
||||
return text.replace(/\s+/g, " ").trim().length;
|
||||
},
|
||||
{ timeout: 15_000 }
|
||||
{ timeout }
|
||||
)
|
||||
.toBeGreaterThan(minimumLength);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ test("@public-live api-v2 gateway and channel API prefixes serve JSON ping respo
|
||||
test("@public-live guest flow renders on the deployed environment", async ({ page }) => {
|
||||
const guards = attachPageHealthGuards(page, baseURL);
|
||||
|
||||
await page.goto("/guest/book/wash");
|
||||
await page.goto(liveUrl("guest/book/wash"));
|
||||
await settlePage(page);
|
||||
await expect(page).toHaveURL(/\/guest\/book\/wash(?:\?.*)?$/);
|
||||
await expectBodyHasContent(page);
|
||||
|
||||
@@ -24,7 +24,7 @@ function toLoopbackRequestUrl(url: string) {
|
||||
}
|
||||
|
||||
async function gotoHealthyRoute(page, path: string) {
|
||||
await page.goto(path);
|
||||
await page.goto(path, { waitUntil: "domcontentloaded" });
|
||||
await settlePage(page);
|
||||
await expectBodyHasContent(page);
|
||||
await expect(page.locator("body")).not.toContainText(/404/i);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const API_HOST =
|
||||
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
||||
/https?:\/\/(?:api(?:-v2)?\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
||||
const TINY_PNG = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAukB9pY9ZxQAAAAASUVORK5CYII=",
|
||||
"base64"
|
||||
@@ -6042,6 +6042,46 @@ export async function mockApi(page, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/release/runtime") && method === "GET") {
|
||||
await route.fulfill(
|
||||
json({
|
||||
data: {
|
||||
source: "local",
|
||||
requested_source: "deployment",
|
||||
generated_at: new Date().toISOString(),
|
||||
trace_id: "playwright-local-prod-runtime",
|
||||
channel: {
|
||||
slug: "stable",
|
||||
name: "Stable",
|
||||
default_channel: true,
|
||||
route_slug: "master",
|
||||
},
|
||||
available_channels: [],
|
||||
versions: {
|
||||
frontend: null,
|
||||
api: null,
|
||||
service_set: null,
|
||||
bundle_id: null,
|
||||
bundle: null,
|
||||
},
|
||||
frontend_base_url: "http://127.0.0.1:4173",
|
||||
api_base_url: "https://api-v2.truckwash.io/master/api",
|
||||
urls: {
|
||||
frontend_base_url: "http://127.0.0.1:4173",
|
||||
api_base_url: "https://api-v2.truckwash.io/master/api",
|
||||
},
|
||||
availability: {
|
||||
configured: true,
|
||||
missing: [],
|
||||
status: "ready",
|
||||
explicit: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/worker/version") && method === "GET") {
|
||||
await route.fulfill(
|
||||
json({
|
||||
|
||||
@@ -60,6 +60,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => {
|
||||
return {
|
||||
SessionUser: {
|
||||
canAccessDepartment: (...args) => canAccessDepartment(...args),
|
||||
canAccessAssignedDepartment: (...args) => canAccessDepartment(...args),
|
||||
functions: {
|
||||
selectDepartment: (...args) => selectDepartment(...args),
|
||||
getDepartmentIdFromUrl: (...args) => getDepartmentIdFromUrl(...args),
|
||||
@@ -149,12 +150,12 @@ describe("NavigationMenuDepartment", () => {
|
||||
it("updates the selected department label when route department changes", async () => {
|
||||
const wrapper = mount(NavigationMenuDepartment);
|
||||
|
||||
expect(wrapper.get("[data-testid='mock-button-label']").text()).toBe("Select department");
|
||||
expect(wrapper.get("[data-testid='mobile-header-department-toggle']").text()).toContain("Select department");
|
||||
expect(__getDepartmentsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
__setDepartmentId("12");
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.get("[data-testid='mock-button-label']").text()).toBe("Taastrup");
|
||||
expect(wrapper.get("[data-testid='mobile-header-department-toggle']").text()).toContain("Taastrup");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,6 +205,9 @@ function mountStepOne() {
|
||||
return mountWithApp(PosDepartmentStep1, {
|
||||
messages: {
|
||||
en: {
|
||||
common: {
|
||||
order: "Order",
|
||||
},
|
||||
admin: {
|
||||
pos: {
|
||||
license_plates: "License plates",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
import ReferenceAutocompletePOS from "@/components/forms/department/pos/input/ReferenceAutocompletePOS.vue";
|
||||
|
||||
@@ -16,8 +16,10 @@ const flushPromises = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
let wrapper = null;
|
||||
|
||||
function mountAutocomplete(props = {}) {
|
||||
return mountWithApp(ReferenceAutocompletePOS, {
|
||||
wrapper = mountWithApp(ReferenceAutocompletePOS, {
|
||||
props: {
|
||||
modelValue: "",
|
||||
departmentId: 12,
|
||||
@@ -27,6 +29,7 @@ function mountAutocomplete(props = {}) {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe("ReferenceAutocompletePOS", () => {
|
||||
@@ -35,6 +38,12 @@ describe("ReferenceAutocompletePOS", () => {
|
||||
requestState.authenticatedRequest.mockResolvedValue({ data: { data: [] } });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
wrapper?.unmount();
|
||||
wrapper = null;
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("fetches async suggestions with the current POS context", async () => {
|
||||
requestState.authenticatedRequest.mockResolvedValue({
|
||||
data: {
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("weatherapi superuser wiring", () => {
|
||||
|
||||
it("appears in superuser configuration navigation menus", () => {
|
||||
expect(navItemsSource).toContain("SessionUser.superUser.modules.weatherapi.meta.labels.multiple");
|
||||
expect(navItemsSource).toContain("to: '/superuser/configuration/weatherapi'");
|
||||
expect(navItemsSource).toMatch(/to:\s*["']\/superuser\/configuration\/weatherapi["']/);
|
||||
|
||||
expect(leftMenuSource).toContain("SessionUser.superUser.modules.weatherapi.meta.title");
|
||||
expect(leftMenuSource).toContain("SessionUser.superUser.modules.weatherapi.meta.config_endpoint");
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("workfeed superuser wiring", () => {
|
||||
|
||||
it("appears in superuser configuration navigation menus", () => {
|
||||
expect(navItemsSource).toContain("SessionUser.superUser.modules.workfeed.meta.labels.multiple");
|
||||
expect(navItemsSource).toContain("to: '/superuser/configuration/workfeed'");
|
||||
expect(navItemsSource).toMatch(/to:\s*["']\/superuser\/configuration\/workfeed["']/);
|
||||
|
||||
expect(leftMenuSource).toContain("SessionUser.superUser.modules.workfeed.meta.title");
|
||||
expect(leftMenuSource).toContain("SessionUser.superUser.modules.workfeed.meta.config_endpoint");
|
||||
|
||||
+55
-3
@@ -37,6 +37,11 @@ const PUBLIC_ASSET_ALIASES = [
|
||||
]
|
||||
|
||||
function getGitCommit() {
|
||||
const explicitCommit = getExplicitCommitSha()
|
||||
if (explicitCommit !== 'unknown') {
|
||||
return explicitCommit.slice(0, 12)
|
||||
}
|
||||
|
||||
try {
|
||||
return execSync('git rev-parse --short HEAD').toString().trim()
|
||||
} catch {
|
||||
@@ -45,6 +50,11 @@ function getGitCommit() {
|
||||
}
|
||||
|
||||
function getGitCommitSha() {
|
||||
const explicitCommit = getExplicitCommitSha()
|
||||
if (explicitCommit !== 'unknown') {
|
||||
return explicitCommit
|
||||
}
|
||||
|
||||
try {
|
||||
return execSync('git rev-parse HEAD').toString().trim()
|
||||
} catch {
|
||||
@@ -52,6 +62,17 @@ function getGitCommitSha() {
|
||||
}
|
||||
}
|
||||
|
||||
function getExplicitCommitSha() {
|
||||
for (const key of ['RELEASE_COMMIT_SHA', 'COMMIT_SHA', 'GITHUB_SHA', 'SOURCE_COMMIT', 'VITE_COMMIT_HASH']) {
|
||||
const value = String(process.env[key] || '').trim()
|
||||
if (/^[a-f0-9]{7,40}$/i.test(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function releaseBuildId(commitSha) {
|
||||
const explicitBuildId = process.env.RELEASE_BUILD_ID || process.env.BUILD_ID
|
||||
if (explicitBuildId) {
|
||||
@@ -66,6 +87,33 @@ function releaseBuildId(commitSha) {
|
||||
return `${prefix}-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`
|
||||
}
|
||||
|
||||
function normalizeViteBase(value) {
|
||||
const base = String(value || '').trim()
|
||||
if (!base || base === '/') {
|
||||
return '/'
|
||||
}
|
||||
if (base === './') {
|
||||
return './'
|
||||
}
|
||||
|
||||
return `/${base.replace(/^\/+|\/+$/g, '')}/`
|
||||
}
|
||||
|
||||
function stripReleaseBasePath(assetPath, base) {
|
||||
if (!assetPath || !assetPath.startsWith('/')) {
|
||||
return assetPath
|
||||
}
|
||||
|
||||
const normalizedBase = normalizeViteBase(base)
|
||||
if (normalizedBase === '/' || normalizedBase === './') {
|
||||
return assetPath
|
||||
}
|
||||
|
||||
return assetPath.startsWith(normalizedBase)
|
||||
? `/${assetPath.slice(normalizedBase.length)}`
|
||||
: assetPath
|
||||
}
|
||||
|
||||
function releaseUrlPath(value, basePath = '/') {
|
||||
if (!value || value.startsWith('#') || /^(?:data|mailto|tel|javascript):/i.test(value)) {
|
||||
return ''
|
||||
@@ -230,6 +278,7 @@ function rootPwaManifestAlias() {
|
||||
|
||||
function releaseMetadataManifest() {
|
||||
let outputDirectory = fromProjectRoot('dist')
|
||||
let releaseBasePath = '/'
|
||||
|
||||
return {
|
||||
name: 'release-metadata-manifest',
|
||||
@@ -238,6 +287,7 @@ function releaseMetadataManifest() {
|
||||
outputDirectory = path.isAbsolute(config.build.outDir)
|
||||
? config.build.outDir
|
||||
: path.resolve(config.root, config.build.outDir)
|
||||
releaseBasePath = config.base || '/'
|
||||
},
|
||||
closeBundle() {
|
||||
const releaseEntryPath = path.join(outputDirectory, 'release-entry.json')
|
||||
@@ -257,11 +307,12 @@ function releaseMetadataManifest() {
|
||||
const rootFiles = fs.readdirSync(outputDirectory)
|
||||
const indexAssetUrls = Array.from(indexHtml.matchAll(/\b(?:href|src)=["']([^"']+)["']/g))
|
||||
.map((match) => releaseUrlPath(match[1], '/index.html'))
|
||||
.map((assetPath) => stripReleaseBasePath(assetPath, releaseBasePath))
|
||||
.filter(Boolean)
|
||||
const releaseEntryAssetUrls = [
|
||||
releaseEntry.entry ? `/${releaseEntry.entry}` : '',
|
||||
...(Array.isArray(releaseEntry.css) ? releaseEntry.css.map((fileName) => `/${fileName}`) : [])
|
||||
].filter(Boolean)
|
||||
].map((assetPath) => stripReleaseBasePath(assetPath, releaseBasePath)).filter(Boolean)
|
||||
const pwaAssetUrls = [
|
||||
'manifest.json',
|
||||
'manifest.webmanifest',
|
||||
@@ -275,6 +326,7 @@ function releaseMetadataManifest() {
|
||||
]
|
||||
.filter((fileName) => fs.existsSync(path.join(outputDirectory, fileName)))
|
||||
.map((fileName) => `/${fileName.replace(/\\/g, '/')}`)
|
||||
.map((assetPath) => stripReleaseBasePath(assetPath, releaseBasePath))
|
||||
|
||||
for (const manifestPath of ['manifest.webmanifest', 'assets/manifest.webmanifest']) {
|
||||
const absoluteManifestPath = path.join(outputDirectory, manifestPath)
|
||||
@@ -287,7 +339,7 @@ function releaseMetadataManifest() {
|
||||
for (const icon of pwaManifest.icons || []) {
|
||||
const iconPath = releaseUrlPath(icon.src || '', `/${manifestPath}`)
|
||||
if (iconPath) {
|
||||
pwaAssetUrls.push(iconPath)
|
||||
pwaAssetUrls.push(stripReleaseBasePath(iconPath, releaseBasePath))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -372,7 +424,7 @@ export default defineConfig(({ mode }) => {
|
||||
process.env.IS_DEV = !isProd ? 'true' : 'false'
|
||||
// Server version updates happen after deploy verification, not during asset builds.
|
||||
// Keep a relative base for static hosting; align manifest with base
|
||||
const base = '/'
|
||||
const base = normalizeViteBase(process.env.VITE_BASE_PATH || process.env.BASE_PATH || '/')
|
||||
const pwaScope = base === './' ? '.' : base
|
||||
|
||||
// Enable single-file build only when explicitly requested (disabled by default for PWA)
|
||||
|
||||
Reference in New Issue
Block a user