Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab09076f5 | ||
|
|
97e17fadd4 | ||
|
|
ef780e5542 | ||
|
|
931c31c7be | ||
|
|
25f2be875c | ||
|
|
ff70326665 | ||
|
|
52ed49211a | ||
|
|
0565fc2a7c | ||
|
|
d30273cda8 | ||
|
|
b893e69b54 | ||
|
|
eabb12ef97 | ||
|
|
225741cf7b |
@@ -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 }}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -34,6 +34,12 @@ export const sourceMappings = [
|
||||
],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "user-vehicles",
|
||||
patterns: [/^src\/components\/displays\/user\/vehicles\//u, /^src\/views\/dashboards\/userDashboard\/vehicles\//u],
|
||||
specs: ["tests/e2e/userVehicles.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "pos",
|
||||
patterns: [/\/pos[/-]/iu, /POS/iu, /^src\/assets\/pos\.css$/u],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { departments, getDepartments} from "@/components/pagination/departmentTabs.vue";
|
||||
import { departments, getDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||
@@ -27,16 +27,15 @@ const props = defineProps({
|
||||
reloadList: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
add_other_customer_id: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const reload = () => {
|
||||
// Load the list of vehicles
|
||||
if (props.reloadList) {
|
||||
@@ -59,13 +58,9 @@ if (departments.value.length === 0) {
|
||||
const onClickListAddons = (vehicleId) => {
|
||||
// Redirect to the product addons page
|
||||
console.log("Fetching product addons for vehicle: " + vehicleId);
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/available',
|
||||
'GET',
|
||||
{
|
||||
id: vehicleId
|
||||
},
|
||||
)
|
||||
SessionUser.request("/vehicles/addons/available", "GET", {
|
||||
id: vehicleId,
|
||||
});
|
||||
};
|
||||
|
||||
const vehicleAddons = ref(null);
|
||||
@@ -76,13 +71,9 @@ const getVehicleAvailableAddons = (vehicleId, forceReload = false) => {
|
||||
// Get the vehicle addons from the server
|
||||
if (vehicleAddons.value === null) {
|
||||
vehicleAddons.value = [];
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/available',
|
||||
'GET',
|
||||
{
|
||||
id: vehicleId
|
||||
},
|
||||
).then((response) => {
|
||||
SessionUser.request("/vehicles/addons/available", "GET", {
|
||||
id: vehicleId,
|
||||
}).then((response) => {
|
||||
console.log("Vehicle addons: ", response.data.data);
|
||||
vehicleAddons.value = response.data.data;
|
||||
});
|
||||
@@ -93,14 +84,10 @@ const getVehicleAvailableAddons = (vehicleId, forceReload = false) => {
|
||||
|
||||
const toggleVehicleAddon = (vehicleId, addonId) => {
|
||||
// Toggle the vehicle addon
|
||||
SessionUser.request(
|
||||
'/vehicles/addons/toggle',
|
||||
'POST',
|
||||
{
|
||||
vehicle_id: vehicleId,
|
||||
addon_id: addonId
|
||||
},
|
||||
).then(() => {
|
||||
SessionUser.request("/vehicles/addons/toggle", "POST", {
|
||||
vehicle_id: vehicleId,
|
||||
addon_id: addonId,
|
||||
}).then(() => {
|
||||
reload();
|
||||
});
|
||||
};
|
||||
@@ -108,9 +95,9 @@ const toggleVehicleAddon = (vehicleId, addonId) => {
|
||||
const getVehicleAddonToggleIcon = (vehicleAddon) => {
|
||||
// Check if the vehicle addon is applied
|
||||
if (isVehicleAddonApplied(vehicleAddon)) {
|
||||
return 'fas fa-minus';
|
||||
return "fas fa-minus";
|
||||
} else {
|
||||
return 'fas fa-plus';
|
||||
return "fas fa-plus";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,140 +124,142 @@ const getProductOptionsLabel = (vehicle) => {
|
||||
<div class="table-container" data-testid="user-vehicles-table-container">
|
||||
<table class="table is-fullwidth" data-testid="user-vehicles-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="!props.compact">{{ $t('objects.columns.id') }}</th>
|
||||
<th v-if="!props.compact">{{ $t('objects.columns.customer_id') }}</th>
|
||||
<th>{{ $t('objects.bookings.columns.reg_1') }}</th>
|
||||
<th>{{ $t('vehicles.type') }}</th>
|
||||
<th>{{ $t('objects.vehicles.columns.wash_subscription') }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||
<th v-if="!props.compact">{{ $t('common.reference') }}</th>
|
||||
<th v-if="!props.compact"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th v-if="!props.compact">{{ $t("objects.columns.id") }}</th>
|
||||
<th v-if="!props.compact">{{ $t("objects.columns.customer_id") }}</th>
|
||||
<th>{{ $t("objects.bookings.columns.reg_1") }}</th>
|
||||
<th>{{ $t("vehicles.type") }}</th>
|
||||
<th>{{ $t("objects.vehicles.columns.wash_subscription") }}</th>
|
||||
<th v-if="!props.compact">{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||
<th v-if="!props.compact">{{ $t("common.reference") }}</th>
|
||||
<th v-if="!props.compact"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="object in props.vehicles" :key="object.id">
|
||||
<!-- ID -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
column="id"
|
||||
/>
|
||||
<!-- Customer ID -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
column="customer_id"
|
||||
/>
|
||||
<!-- Reg -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reg"
|
||||
/>
|
||||
<!-- Type -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="type"
|
||||
:parse-function="(value) => {
|
||||
return SessionUser.objects.products.functions.getProductName(value, 'Ukendt');
|
||||
}"
|
||||
/>
|
||||
<!-- Wash Subscription -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="wash_subscription"
|
||||
:parse-function="(value) => {
|
||||
return value ? t('common.yes') : t('common.no');
|
||||
}"
|
||||
/>
|
||||
<!-- Product Options, if the wash subscription is set to true -->
|
||||
<td v-if="!props.compact">
|
||||
<template v-if="object.wash_subscription">
|
||||
<!-- Enabled subscription -->
|
||||
<ActionSettingsWheelButton
|
||||
:label="getProductOptionsLabel(object)"
|
||||
:icon="SessionUser.objects.product_options.meta.icon"
|
||||
@mouseenter="getVehicleAvailableAddons(object.id, true)"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- List addons -->
|
||||
<template v-for="vehicleAddon in vehicleAddons" :key="vehicleAddon.id">
|
||||
<ActionSettingsWheelItem
|
||||
:label="(isVehicleAddonApplied(vehicleAddon) ? SessionUser.objects.global.language.remove : SessionUser.objects.global.language.add) + ' ' + vehicleAddon.name"
|
||||
:icon="getVehicleAddonToggleIcon(vehicleAddon)"
|
||||
:click-action="() => toggleVehicleAddon(object.id, vehicleAddon.id)"
|
||||
:template="isVehicleAddonApplied(vehicleAddon) ? 'danger' : 'default'"
|
||||
/>
|
||||
<tr v-for="object in props.vehicles" :key="object.id">
|
||||
<!-- ID -->
|
||||
<EditableTableColumn v-if="!props.compact" :object="object" :loadList="reload" column="id" />
|
||||
<!-- Customer ID -->
|
||||
<EditableTableColumn v-if="!props.compact" :object="object" :loadList="reload" column="customer_id" />
|
||||
<!-- Reg -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reg"
|
||||
/>
|
||||
<!-- Type -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="type"
|
||||
:parse-function="
|
||||
(value) => {
|
||||
return SessionUser.objects.products.functions.getProductName(value, 'Ukendt');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!-- Wash Subscription -->
|
||||
<EditableTableColumn
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="wash_subscription"
|
||||
:parse-function="
|
||||
(value) => {
|
||||
return value ? t('common.yes') : t('common.no');
|
||||
}
|
||||
"
|
||||
/>
|
||||
<!-- Product Options, if the wash subscription is set to true -->
|
||||
<td v-if="!props.compact">
|
||||
<template v-if="object.wash_subscription">
|
||||
<!-- Enabled subscription -->
|
||||
<ActionSettingsWheelButton
|
||||
:label="getProductOptionsLabel(object)"
|
||||
:icon="SessionUser.objects.product_options.meta.icon"
|
||||
@mouseenter="getVehicleAvailableAddons(object.id, true)"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- List addons -->
|
||||
<template v-for="vehicleAddon in vehicleAddons" :key="vehicleAddon.id">
|
||||
<ActionSettingsWheelItem
|
||||
:label="
|
||||
(isVehicleAddonApplied(vehicleAddon)
|
||||
? SessionUser.objects.global.language.remove
|
||||
: SessionUser.objects.global.language.add) +
|
||||
' ' +
|
||||
vehicleAddon.name
|
||||
"
|
||||
:icon="getVehicleAddonToggleIcon(vehicleAddon)"
|
||||
:click-action="() => toggleVehicleAddon(object.id, vehicleAddon.id)"
|
||||
:template="isVehicleAddonApplied(vehicleAddon) ? 'danger' : 'default'"
|
||||
/>
|
||||
</template>
|
||||
<!-- If there are no addons, show a message -->
|
||||
<ActionSettingsWheelItem
|
||||
v-if="vehicleAddons ? vehicleAddons.length === 0 : true"
|
||||
:label="$t('global.no_data')"
|
||||
icon="fas fa-list"
|
||||
/>
|
||||
<!-- Addons -->
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</template>
|
||||
<!-- If there are no addons, show a message -->
|
||||
<ActionSettingsWheelItem
|
||||
v-if="vehicleAddons ? vehicleAddons.length === 0 : true"
|
||||
:label="$t('global.no_data')"
|
||||
icon="fas fa-list"
|
||||
/>
|
||||
<!-- Addons -->
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Disabled subscription -->
|
||||
{{ $t('global.no_data') }}
|
||||
</template>
|
||||
</td>
|
||||
<!-- Reference to the vehicle -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reference"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<!-- Actions Should not be shown directly! -->
|
||||
<ActionSettingsWheelButton
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="false"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.manage') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-eye"
|
||||
:click-action="() => redirectUserVehiclePage(object.id)"
|
||||
/>
|
||||
<!-- Delete -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.delete') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-trash"
|
||||
:template="'danger'"
|
||||
:click-action="() => SessionUser.objects.vehicles.functions.showDeleteObjectForm(object.id, () => reload())"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<!-- Disabled subscription -->
|
||||
{{ $t("global.no_data") }}
|
||||
</template>
|
||||
</td>
|
||||
<!-- Reference to the vehicle -->
|
||||
<EditableTableColumn
|
||||
v-if="!props.compact"
|
||||
:object="object"
|
||||
:loadList="reload"
|
||||
:editFunction="SessionUser.objects.vehicles.showEditObjectFieldForm"
|
||||
column="reference"
|
||||
/>
|
||||
<!-- Actions -->
|
||||
<td>
|
||||
<!-- Actions stay grouped behind the wheel menu. -->
|
||||
<ActionSettingsWheelButton
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="false"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.manage') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-eye"
|
||||
:click-action="() => redirectUserVehiclePage(object.id)"
|
||||
/>
|
||||
<!-- Delete -->
|
||||
<ActionSettingsWheelItem
|
||||
:label="$t('global.delete') + ' ' + $t('objects.vehicles.single')"
|
||||
icon="fas fa-trash"
|
||||
:template="'danger'"
|
||||
:click-action="
|
||||
() => SessionUser.objects.vehicles.functions.showDeleteObjectForm(object.id, () => reload())
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</ActionSettingsWheelButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="10">{{ $t('tables.showing') }} {{ props.vehicles.length }} {{ $t('objects.vehicles.multiple') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
{{ $t("tables.showing") }} {{ props.vehicles.length }} {{ $t("objects.vehicles.multiple") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></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
|
||||
);
|
||||
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sourceMappings } from "../../scripts/playwright-pr-mapping.mjs";
|
||||
|
||||
const specsFor = (file) =>
|
||||
sourceMappings
|
||||
.filter((mapping) => mapping.patterns.some((pattern) => pattern.test(file)))
|
||||
.flatMap((mapping) => mapping.specs);
|
||||
|
||||
describe("Playwright PR mapping", () => {
|
||||
it("maps user vehicle table changes to the user vehicles E2E coverage", () => {
|
||||
expect(specsFor("src/components/displays/user/vehicles/vehiclesTable.vue")).toContain(
|
||||
"tests/e2e/userVehicles.spec.ts"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,8 @@ describe("self-serve studio condition scope", () => {
|
||||
'const nodeScopeLabel = (data) => scopeParts(data).join(" / ") || data?.subtitle || "Shared scope";'
|
||||
);
|
||||
expect(studioSource).toContain("v-else-if=\"selectedNode.data.kind === 'condition'\"");
|
||||
expect(studioSource).toContain("<div><dt>Scope</dt><dd>{{ nodeScopeLabel(data) }}</dd></div>");
|
||||
expect(studioSource).toContain("<dt>Scope</dt>");
|
||||
expect(studioSource).toContain("<dd>{{ nodeScopeLabel(data) }}</dd>");
|
||||
});
|
||||
|
||||
it("builds conditions with expression groups, branches, and cases", () => {
|
||||
|
||||
@@ -11,9 +11,17 @@ describe("self-serve studio managed inspector", () => {
|
||||
it("routes non-graph self-serve nodes to managed inspector panels", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain(
|
||||
'const managedInspectorNodeKinds = ["runtime_checkpoint", "vehicle_type", "machine_type", "edge_gateway", "relay_binding", "relay"];'
|
||||
);
|
||||
expect(source).toContain("const managedInspectorNodeKinds = [");
|
||||
for (const nodeKind of [
|
||||
"runtime_checkpoint",
|
||||
"vehicle_type",
|
||||
"machine_type",
|
||||
"edge_gateway",
|
||||
"relay_binding",
|
||||
"relay",
|
||||
]) {
|
||||
expect(source).toContain(`"${nodeKind}"`);
|
||||
}
|
||||
expect(source).toContain('v-else-if="isSelectedManaged"');
|
||||
expect(source).toContain('data-testid="studio-managed-inspector"');
|
||||
expect(source).toContain('data-testid="studio-runtime-inspector"');
|
||||
@@ -42,7 +50,8 @@ describe("self-serve studio managed inspector", () => {
|
||||
expect(source).toContain('return applyVirtualHardwareOperation("upsert_gateway", {');
|
||||
expect(source).toContain("const saveSelectedVirtualBinding = () => {");
|
||||
expect(source).toContain('return applyVirtualHardwareOperation("upsert_binding", payload);');
|
||||
expect(source).toContain("const deleteSelectedVirtualBinding = () => deleteVirtualBinding({");
|
||||
expect(source).toContain("const deleteSelectedVirtualBinding = () =>");
|
||||
expect(source).toContain("deleteVirtualBinding({");
|
||||
expect(source).toContain("const selectedGatewayBindingEntries = computed(() => {");
|
||||
expect(source).toContain("const selectedRelayBindingEntries = computed(() => {");
|
||||
});
|
||||
|
||||
@@ -46,9 +46,8 @@ describe("self-serve studio task editing", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("getSelfServeDynamicImageButtonsToPress(simulatorPreviewTasks.value)");
|
||||
expect(source).toContain(
|
||||
"const simulatorDynamicImageCurrentStep = computed(() => getSelfServeCompletedDynamicImageStep("
|
||||
);
|
||||
expect(source).toContain("const simulatorDynamicImageCurrentStep = computed(() =>");
|
||||
expect(source).toContain("getSelfServeCompletedDynamicImageStep(");
|
||||
expect(source).toContain("const simulatorDynamicImageUrl = computed(() => {");
|
||||
expect(source).toContain("buildSelfServeDynamicImageUrl({");
|
||||
expect(source).toContain("currentStep: simulatorDynamicImageCurrentStep.value");
|
||||
@@ -77,10 +76,10 @@ describe("self-serve studio task editing", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("const scopeFiltersFromRoute = () => ({");
|
||||
expect(source).toContain("const showScopeGuide = computed(() => (");
|
||||
expect(source).toContain("const showScopeGuide = computed(");
|
||||
expect(source).toContain("const applyScopeGuideSelection = () => {");
|
||||
expect(source).toContain("const writeScopeToRoute = () => {");
|
||||
expect(source).toContain("const simulatorStorageKey = computed(() => [");
|
||||
expect(source).toContain("const simulatorStorageKey = computed(() =>");
|
||||
expect(source).toContain("const restoreSimulatorStateFromStorage = () => {");
|
||||
expect(source).toContain("const persistSimulatorStateToStorage = () => {");
|
||||
expect(source).toContain('data-testid="studio-scope-guide"');
|
||||
@@ -93,7 +92,7 @@ describe("self-serve studio task editing", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("const taskAttachmentList = (dataOrRaw) => {");
|
||||
expect(source).toContain("const selectedTaskAttachments = computed(() => (");
|
||||
expect(source).toContain("const selectedTaskAttachments = computed(() =>");
|
||||
expect(source).toContain("const uploadSelectedTaskAttachment = async (event) => {");
|
||||
expect(source).toContain("const deleteSelectedTaskAttachment = async (attachment) => {");
|
||||
expect(source).toContain('data-testid="studio-task-node-attachments"');
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("self-serve studio vehicle type scope", () => {
|
||||
|
||||
expect(source).toContain("const selectedSimulatorLaneId = computed(() => firstPositiveInt(");
|
||||
expect(source).toContain("filters.value.lane_id");
|
||||
expect(source).toContain("const selectedSimulatorVehicleTypeId = computed(() => (");
|
||||
expect(source).toContain("const selectedSimulatorVehicleTypeId = computed(() =>");
|
||||
expect(source).toContain("vehicleTypeMatchesFilter(vehicleType, selectedValue)");
|
||||
expect(source).toContain("lane_id: laneId");
|
||||
expect(source).toContain("vehicle_type_id: selectedSimulatorVehicleTypeId.value");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user