Compare commits

...
12 changed files with 184 additions and 136 deletions
+6 -26
View File
@@ -18,15 +18,14 @@ jobs:
build-upload-and-verify:
runs-on: [self-hosted, Linux, X64, default]
env:
RELEASE_BASE_URL: https://dev.truckwash.io
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
RELEASE_WAIT_INITIAL_SECONDS: 30
RELEASE_WAIT_TIMEOUT_SECONDS: 300
RELEASE_WAIT_INITIAL_SECONDS: 45
RELEASE_WAIT_TIMEOUT_SECONDS: 600
RELEASE_POLL_INTERVAL_SECONDS: 10
steps:
- name: Checkout repository
@@ -72,26 +71,7 @@ jobs:
path: dist
retention-days: 14
- name: Install lftp
run: |
if ! command -v lftp >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y lftp
fi
- name: Upload hashed assets before release metadata
run: |
test -n "$RELEASE_DEPLOY_HOST" || (echo "RELEASE_DEPLOY_HOST is required" >&2; exit 1)
test -n "$RELEASE_DEPLOY_USER" || (echo "RELEASE_DEPLOY_USER is required" >&2; exit 1)
test -n "$RELEASE_DEPLOY_PASSWORD" || (echo "RELEASE_DEPLOY_PASSWORD is required" >&2; exit 1)
npm run release:upload:lftp
env:
RELEASE_DEPLOY_HOST: ${{ secrets.RELEASE_DEPLOY_HOST }}
RELEASE_DEPLOY_USER: ${{ secrets.RELEASE_DEPLOY_USER }}
RELEASE_DEPLOY_PASSWORD: ${{ secrets.RELEASE_DEPLOY_PASSWORD }}
RELEASE_DEPLOY_REMOTE_ROOT: ${{ secrets.RELEASE_DEPLOY_REMOTE_ROOT }}
- name: Wait for exact uploaded build
- name: Wait for Coolify release artifact
run: npm run release:verify-upload
- name: Public live Playwright gate
@@ -118,7 +98,7 @@ jobs:
-X POST "$RELEASE_MANAGER_GATE_URL" \
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
env:
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
+24 -4
View File
@@ -65,15 +65,35 @@ jobs:
if: github.event_name != 'schedule'
needs: build-and-unit
runs-on: [self-hosted, Linux, X64, default]
env:
PLAYWRIGHT_PR_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
PLAYWRIGHT_PR_HEAD: ${{ github.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve Playwright diff refs
id: playwright-diff
shell: bash
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
zero_sha="0000000000000000000000000000000000000000"
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
base_ref="$PR_BASE_SHA"
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
base_ref="origin/$DEFAULT_BRANCH"
else
base_ref="$PUSH_BEFORE_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
with:
@@ -87,7 +107,7 @@ jobs:
run: npx playwright install --with-deps chromium
- name: Run Playwright PR tests
run: npm run test:e2e:pr -- --base="$PLAYWRIGHT_PR_BASE" --head="$PLAYWRIGHT_PR_HEAD"
run: npm run test:e2e:pr -- --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}"
- name: Upload Playwright report
if: failure()
+8
View File
@@ -8,6 +8,14 @@ COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
ARG SOURCE_COMMIT
ARG RELEASE_COMMIT_SHA
ARG COMMIT_SHA
ARG GITHUB_SHA
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ENV RELEASE_COMMIT_SHA=$RELEASE_COMMIT_SHA
ENV COMMIT_SHA=$COMMIT_SHA
ENV GITHUB_SHA=$GITHUB_SHA
RUN npm run build
FROM nginx:1.27-alpine
+1 -1
View File
@@ -120,7 +120,7 @@ async function verifyShell(baseUrl, shellPath) {
async function verifyRelease(baseUrl) {
const expectedCommit = process.env.RELEASE_EXPECTED_COMMIT || process.env.GITHUB_SHA || "";
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || process.env.RELEASE_BUILD_ID || "";
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || "";
const manifest = await fetchJson(baseUrl, "release-manifest.json");
const releaseEntry = await fetchJson(baseUrl, "release-entry.json");
@@ -43,9 +43,10 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
<div class="self-serve-questions-status-slot" aria-live="polite">
<div
v-if="isLoading"
class="notification is-info is-light py-2 px-3 mb-0"
:class="{ 'is-invisible': !isLoading }"
data-testid="self-serve-questions-inline-loading"
:aria-hidden="!isLoading"
>
<b-icon pack="fas" icon="spinner" custom-class="fa-pulse" size="is-small" />
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
@@ -72,4 +73,8 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
.self-serve-questions-status-slot .notification {
width: 100%;
}
.self-serve-questions-status-slot .notification.is-invisible {
visibility: hidden;
}
</style>
@@ -519,9 +519,11 @@ const hasConfirmedCurrentQuestionReview = computed(
const shouldReturnToQuestionsForReview = computed(
() =>
!isCompletingWash.value &&
!washInProgress.value &&
visibleQuestions.value.length > 0 &&
currentStep.value > steps.QUESTIONS &&
currentStep.value < steps.COMPLETED &&
!hasConfirmedCurrentQuestionReview.value
);
+5 -77
View File
@@ -761,86 +761,14 @@ async function installCoolifyMocks(page, state) {
}
test.describe("Coolify infrastructure management", () => {
test("@smoke configures and tests a Coolify connection with mocked API data", async ({ page }) => {
const state = await boot(page);
test("@smoke opens the current Coolify integration surface from the legacy route", async ({ page }) => {
await boot(page);
await page.goto("/superuser/configuration/coolify");
await expect(page.getByRole("heading", { name: "Coolify configuration" })).toBeVisible({ timeout: 30_000 });
await page.locator(".card-header").filter({ hasText: "API connection" }).click();
await page.locator(".card-header").filter({ hasText: "Gateway load balancer" }).click();
await page.locator(".card-header").filter({ hasText: "Instances" }).click();
await page.locator(".card-header").filter({ hasText: "Managed targets" }).click();
await expect(page.getByTestId("coolify-load-balancer-card")).toContainText("api-v2.truckwash.io");
await expect(page.getByTestId("coolify-gateways-table")).toContainText("65.21.214.30");
await expect(page.getByTestId("coolify-load-balancer-drift")).toContainText("Add target 65.21.214.30");
await page.getByRole("button", { name: "Dry-run reconcile" }).click();
expect(state.lbReconciles.at(-1)).toEqual({ dry_run: true });
await page.getByRole("button", { name: "Enforce reconcile" }).click();
expect(state.lbReconciles.at(-1)).toEqual({ dry_run: false });
await expect(page.getByTestId("coolify-load-balancer-card")).toContainText("ok");
await expect(page.getByTestId("coolify-load-balancer-drift")).toContainText("No planned changes.");
await page.getByRole("button", { name: "Dry-run route deploy" }).click();
expect(state.routeDeploys.at(-1)).toEqual({ dry_run: true });
expect(state.routePlans.at(-1)).toEqual(
expect.arrayContaining([
expect.objectContaining({
public_url: "https://api-v2.truckwash.io/internal/api",
frontend_public_url: "https://api-v2.truckwash.io/internal/frontend",
}),
])
);
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Route plan");
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Warnings 1");
await page.getByRole("button", { name: "Deploy api-v2 route" }).click();
expect(state.routeDeploys.at(-1)).toEqual({ dry_run: false });
expect(state.routePlans.at(-1)).toEqual(
expect.arrayContaining([
expect.objectContaining({
public_url: "https://api-v2.truckwash.io/internal/api",
frontend_public_url: "https://api-v2.truckwash.io/internal/frontend",
}),
])
);
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Route deploy");
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Applied 1");
await page.getByRole("button", { name: "Deploy latest API code" }).click();
expect(state.apiCodeDeploys.at(-1)).toEqual({ dry_run: false, deploy_routes: true });
expect(state.apiCodePlans.at(-1)).toEqual(
expect.arrayContaining([
expect.objectContaining({
public_url: "https://api-v2.truckwash.io/internal/api",
}),
])
);
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("API code deploy");
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("Applied 1");
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("Route applied 1");
await expect(page.getByTestId("coolify-instances-table")).toContainText("Production Coolify");
await expect(page.getByTestId("coolify-targets-table")).toContainText("failover_ready");
const form = page.getByTestId("coolify-instance-form");
await form.locator('input[type="text"]').nth(0).fill("Staging Coolify");
await form.locator('input[type="url"]').fill("https://staging-coolify.example.test");
await form.locator('input[type="password"]').fill("token-secret");
await expect(form.getByText("Project UUID")).toHaveCount(0);
await expect(form.getByText("Server UUID")).toHaveCount(0);
await form.getByRole("button", { name: "Save" }).click();
await expect(page.getByTestId("coolify-instances-table")).toContainText("Staging Coolify");
await page.getByTestId("coolify-instances-table").getByRole("button", { name: "Test" }).first().click();
await expect(page.getByTestId("coolify-instances-table")).toContainText("ok");
await page.getByTestId("coolify-targets-table").getByRole("button", { name: "Reconcile" }).first().click();
await expect.poll(() => state.reconciledTargets).toContain(7);
await expect(page.getByTestId("coolify-targets-table")).toContainText("failover_ready");
page.once("dialog", async (dialog) => {
expect(dialog.message()).toContain("delete-coolify-target-7");
await dialog.accept("delete-coolify-target-7");
});
await page.getByTestId("coolify-targets-table").getByRole("button", { name: "Delete" }).first().click();
await expect(page.getByTestId("coolify-targets-table")).toContainText("No Coolify targets configured.");
await expect(page).toHaveURL(/\/superuser\/configuration\/releases\/integrations\?panel=coolify/);
await expect(page.getByRole("heading", { name: "Release Manager" })).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId("release-coolify-panel")).toContainText("Coolify");
});
test("@smoke creates Coolify-backed database, Redis, and MinIO replicas from replication management", async ({
+58 -10
View File
@@ -5,6 +5,57 @@ async function primeSuperuserSession(page) {
await primeMockSession(page, { token: "superuser-edge-gateway-token" });
}
const transientDynamicImportPattern = /(?:Failed to fetch dynamically imported module|ERR_NETWORK_CHANGED)/iu;
async function gotoEdgeAgentTerminal(page, gatewayId = 701) {
const terminalPath = `/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/terminal`;
const loadErrors = [];
const recordLoadError = (error) => {
const message = typeof error === "string" ? error : error?.message || String(error || "");
if (transientDynamicImportPattern.test(message)) {
loadErrors.push(message);
}
};
const onConsole = (message) => {
if (message.type() === "error") {
recordLoadError(message.text());
}
};
const onPageError = (error) => recordLoadError(error);
page.on("console", onConsole);
page.on("pageerror", onPageError);
try {
for (let attempt = 0; attempt < 3; attempt += 1) {
loadErrors.length = 0;
let navigationError = null;
try {
await page.goto(terminalPath, { waitUntil: "domcontentloaded" });
} catch (error) {
navigationError = error;
recordLoadError(error);
}
if (navigationError && loadErrors.length === 0) {
throw navigationError;
}
const terminalVisible = await page
.getByTestId("gateway-terminal-page")
.isVisible({ timeout: attempt === 0 ? 5_000 : 10_000 })
.catch(() => false);
if (terminalVisible || loadErrors.length === 0) {
return;
}
}
} finally {
page.off("console", onConsole);
page.off("pageerror", onPageError);
}
}
async function selectLastGatewayBindingRelay(page, relayId) {
const relaySelect = page.locator('[data-testid^="gateway-binding-relay-"]').last();
await expect
@@ -919,14 +970,11 @@ test.describe("Edge gateway management smoke", () => {
};
};
await page.route(
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/modules\/edge-gateways\/workspace\/departments\/1(?:\?.*)?$/i,
async (route) => {
await route.fulfill(json({ data: buildGateWorkspace() }));
}
);
await page.route(/\/modules\/edge-gateways\/workspace\/departments\/1(?:\?.*)?$/i, async (route) => {
await route.fulfill(json({ data: buildGateWorkspace() }));
});
await page.route(/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/department\/gates(?:\?.*)?$/i, async (route) => {
await page.route(/\/department\/gates(?:\?.*)?$/i, async (route) => {
const request = route.request();
const method = request.method();
@@ -1104,7 +1152,7 @@ test.describe("Edge gateway management smoke", () => {
});
await primeSuperuserSession(page);
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
await gotoEdgeAgentTerminal(page);
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/not connected to the broker/i);
@@ -1133,7 +1181,7 @@ test.describe("Edge gateway management smoke", () => {
});
await primeSuperuserSession(page);
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
await gotoEdgeAgentTerminal(page);
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/Broker did not complete/i, {
@@ -1154,7 +1202,7 @@ test.describe("Edge gateway management smoke", () => {
});
await primeSuperuserSession(page);
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
await gotoEdgeAgentTerminal(page);
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/connecting|open/i);
+16 -6
View File
@@ -2304,7 +2304,8 @@ test.describe("All-in-one self-serve studio", () => {
};
});
expect(laneScopeOptionMetrics.bottomGap).toBeLessThanOrEqual(2);
expect(laneScopeOptionMetrics.optionHeight).toBeGreaterThan(80);
const minimumScopeOptionHeight = (page.viewportSize()?.width ?? 1024) < 640 ? 44 : 80;
expect(laneScopeOptionMetrics.optionHeight).toBeGreaterThanOrEqual(minimumScopeOptionHeight);
await page.getByTestId("studio-scope-option-lane-7").click();
await page.getByTestId("studio-scope-option-vehicle-8").click();
await page.getByTestId("studio-scope-apply").click();
@@ -2847,20 +2848,29 @@ test.describe("All-in-one self-serve studio", () => {
const sidebar = document.querySelector(".studio-sidebar")?.getBoundingClientRect();
const workspace = document.querySelector('[data-testid="studio-simulator-workspace"]')?.getBoundingClientRect();
return {
shellLeft: shell?.left || 0,
shellRight: shell?.right || 0,
shellWidth: shell?.width || 0,
sidebarBottom: sidebar?.bottom || 0,
sidebarRight: sidebar?.right || 0,
sidebarWidth: sidebar?.width || 0,
workspaceLeft: workspace?.left || 0,
workspaceRight: workspace?.right || 0,
workspaceTop: workspace?.top || 0,
workspaceWidth: workspace?.width || 0,
};
});
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarRight - 4);
expect(simulatorLayoutMetrics.workspaceRight).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellRight - 8);
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(
simulatorLayoutMetrics.shellWidth - simulatorLayoutMetrics.sidebarWidth - 40
);
if ((page.viewportSize()?.width ?? 1024) < 820) {
expect(simulatorLayoutMetrics.workspaceTop).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarBottom - 4);
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellLeft - 8);
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(simulatorLayoutMetrics.shellWidth - 40);
} else {
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarRight - 4);
expect(simulatorLayoutMetrics.workspaceRight).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellRight - 8);
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(
simulatorLayoutMetrics.shellWidth - simulatorLayoutMetrics.sidebarWidth - 40
);
}
await expect(page.getByTestId("studio-hardware-mode")).toContainText("Studio");
await expect(page.getByTestId("studio-simulator-scope")).toContainText("Lane 7");
await expect(page.getByTestId("studio-simulator-scope")).toContainText("Bus");
+34 -9
View File
@@ -50,18 +50,20 @@ async function advanceGuidedWashToLastStep(page) {
}
}
function waitForLaneCommandRequest(page, command) {
return page.waitForRequest((request) => {
if (!request.url().includes("/modules/self-serve/lane/command")) {
async function waitForLaneCommandRequest(page, command) {
const request = await page.waitForRequest((candidateRequest) => {
if (!candidateRequest.url().includes("/modules/self-serve/lane/command")) {
return false;
}
if (request.method() !== "POST") {
if (candidateRequest.method() !== "POST") {
return false;
}
const body = request.postDataJSON?.() || {};
const body = candidateRequest.postDataJSON?.() || {};
return body.command === command;
});
await request.response();
return request;
}
function buildSavedProgress(overrides = {}) {
@@ -234,7 +236,7 @@ test.describe("Self-serve wash", () => {
lane_id: 7,
command: "OPEN_PROPERTY_EXIT_GATE",
});
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
await page.getByTestId("self-serve-nav-close").click();
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible();
@@ -857,7 +859,7 @@ test.describe("Self-serve wash", () => {
command: "OPEN_PROPERTY_EXIT_GATE",
});
await expect(page.getByTestId("self-serve-action-error")).toBeHidden();
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
});
@@ -981,6 +983,7 @@ test.describe("Self-serve wash", () => {
permissions: ["user"],
selfServe: true,
});
api.selfServe.answerResponseDelayMs = 1000;
const preview = api.selfServe.previewByKey["7:AB12345"];
const primaryQuestion = preview.questions[0];
const followUpQuestion = {
@@ -1014,7 +1017,29 @@ test.describe("Self-serve wash", () => {
.poll(async () => questionCards.evaluateAll((cards) => cards.map((card) => card.getAttribute("data-testid"))))
.toEqual(["self-serve-question-11", "self-serve-question-21"]);
const firstQuestionBefore = await page.getByTestId("self-serve-question-11").boundingBox();
const firstQuestion = page.getByTestId("self-serve-question-11");
let stableQuestionBoxSamples = 0;
let previousQuestionBoxKey = "";
await expect
.poll(
async () => {
const [box, viewport] = await Promise.all([firstQuestion.boundingBox(), page.viewportSize()]);
if (!box || !viewport || box.x < 0 || box.x >= viewport.width / 2) {
stableQuestionBoxSamples = 0;
previousQuestionBoxKey = "";
return false;
}
const boxKey = [box.x, box.y, box.width, box.height].map((value) => Math.round(value)).join(":");
stableQuestionBoxSamples = boxKey === previousQuestionBoxKey ? stableQuestionBoxSamples + 1 : 0;
previousQuestionBoxKey = boxKey;
return stableQuestionBoxSamples >= 2;
},
{ timeout: 10_000 }
)
.toBe(true);
const firstQuestionBefore = await firstQuestion.boundingBox();
expect(firstQuestionBefore).not.toBeNull();
const reorderedAnsweredQuestions = [{ ...followUpQuestion }, { ...primaryQuestion, answer: false }];
@@ -1034,7 +1059,7 @@ test.describe("Self-serve wash", () => {
await expect(noButton).toHaveClass(/is-danger/);
await expect(noButton).not.toHaveClass(/is-light/);
await expect(page.getByTestId("self-serve-questions-inline-loading")).toBeVisible();
const firstQuestionWhileLoading = await page.getByTestId("self-serve-question-11").boundingBox();
const firstQuestionWhileLoading = await firstQuestion.boundingBox();
expect(firstQuestionWhileLoading).not.toBeNull();
expect(Math.abs((firstQuestionWhileLoading?.y || 0) - (firstQuestionBefore?.y || 0))).toBeLessThanOrEqual(4);
@@ -851,13 +851,15 @@ test.describe("Superuser system status smoke", () => {
await expect(page.getByTestId("replication-host-database-2").locator(".buttons.are-small")).toHaveCount(0);
await expect(primaryActions.locator(".action-settings-wheel-trigger")).toBeVisible();
await primaryActions.locator(".action-settings-wheel-trigger").click();
await expect(primaryActions.locator(".dropdown-item-action:visible")).toHaveCount(1);
await expect(primaryActions.locator(".dropdown-item-action:visible")).toHaveCount(2);
await expect(primaryActions.getByRole("button", { name: "Test" })).toBeVisible();
await expect(primaryActions.getByRole("button", { name: "Omdøb" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(replicaActions.locator(".action-settings-wheel-trigger")).toBeVisible();
const replicaActionTrigger = replicaActions.locator(".action-settings-wheel-trigger");
await replicaActionTrigger.click();
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(3);
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4);
await expect(replicaActions.getByRole("button", { name: "Omdøb" })).toBeVisible();
const [triggerBox, menuBox] = await Promise.all([
replicaActionTrigger.boundingBox(),
replicaActions.locator(".dropdown-content").boundingBox(),
+20
View File
@@ -37,6 +37,11 @@ const PUBLIC_ASSET_ALIASES = [
]
function getGitCommit() {
const envCommit = firstReleaseCommitEnv()
if (envCommit) {
return envCommit.slice(0, 12)
}
try {
return execSync('git rev-parse --short HEAD').toString().trim()
} catch {
@@ -45,6 +50,11 @@ function getGitCommit() {
}
function getGitCommitSha() {
const envCommit = firstReleaseCommitEnv()
if (envCommit) {
return envCommit
}
try {
return execSync('git rev-parse HEAD').toString().trim()
} catch {
@@ -52,6 +62,16 @@ function getGitCommitSha() {
}
}
function firstReleaseCommitEnv() {
for (const key of ['RELEASE_COMMIT_SHA', 'SOURCE_COMMIT', 'COMMIT_SHA', 'GITHUB_SHA', 'VITE_COMMIT_HASH']) {
const value = String(process.env[key] || '').trim()
if (/^[0-9a-f]{7,40}$/i.test(value)) {
return value
}
}
return ''
}
function releaseBuildId(commitSha) {
const explicitBuildId = process.env.RELEASE_BUILD_ID || process.env.BUILD_ID
if (explicitBuildId) {