Fix map recovery and clean transient test resources
This commit is contained in:
parent
1db67b1048
commit
e2bc8acf97
|
|
@ -14,14 +14,15 @@ TRAEFIK_ROUTER_RULE='PathPrefix(`/`)'
|
||||||
PHX_HOST=proxy
|
PHX_HOST=proxy
|
||||||
PHX_SCHEME=https
|
PHX_SCHEME=https
|
||||||
PHX_URL_PORT=443
|
PHX_URL_PORT=443
|
||||||
MAP_TILE_URL=https://proxy/__e2e__/map-tile.png?z={z}&x={x}&y={y}
|
MAP_TILE_URL=/__e2e__/map-tile.png?z={z}&x={x}&y={y}
|
||||||
|
|
||||||
DEPLOYMENT_TARGET=compose
|
DEPLOYMENT_TARGET=compose
|
||||||
DEPLOYMENT_ENV=development
|
DEPLOYMENT_ENV=development
|
||||||
COMPOSE_PROJECT_NAME=who_need_help_e2e
|
COMPOSE_PROJECT_NAME=who_need_help_e2e
|
||||||
APP_IMAGE=who-need-help:e2e
|
APP_IMAGE=who-need-help:e2e-GENERATED_UNIQUE_E2E_RUN
|
||||||
SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-e2e
|
SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-e2e-GENERATED_UNIQUE_E2E_RUN
|
||||||
POSTGIS_IMAGE=who-need-help:postgis-e2e
|
POSTGIS_IMAGE=who-need-help:postgis-e2e-GENERATED_UNIQUE_E2E_RUN
|
||||||
|
E2E_TEST_IMAGE=who-need-help-e2e-tests:GENERATED_UNIQUE_E2E_RUN
|
||||||
APP_TOPOLOGY=split
|
APP_TOPOLOGY=split
|
||||||
DATABASE_MODE=container
|
DATABASE_MODE=container
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,29 @@ html {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-unavailable-panel {
|
||||||
|
display: grid;
|
||||||
|
min-height: inherit;
|
||||||
|
place-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0.875rem;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-unavailable-panel p {
|
||||||
|
max-width: 34rem;
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 68%, transparent);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-retry-button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: color-mix(in oklab, var(--color-base-100) 94%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.home-demo-map-fallback {
|
.home-demo-map-fallback {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -406,6 +429,11 @@ html {
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-demo-map-fallback > .map-retry-button {
|
||||||
|
justify-self: start;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.home-demo-map-fallback-point {
|
.home-demo-map-fallback-point {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -607,8 +635,7 @@ html {
|
||||||
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
|
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-location-map-shell:has(.request-location-map[data-map-unavailable="true"])
|
.request-location-workspace[data-map-unavailable="true"] .request-location-map-hint {
|
||||||
.request-location-map-hint {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,27 @@ const privacySafeAreaCenter = (latitude, longitude, radiusMeters) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapRetryButton = (element, retry) => {
|
||||||
|
const button = document.createElement("button")
|
||||||
|
button.type = "button"
|
||||||
|
button.className = "btn btn-sm btn-outline map-retry-button"
|
||||||
|
button.dataset.mapRetry = "true"
|
||||||
|
button.textContent = element.dataset.mapRetryLabel || "Try map again"
|
||||||
|
button.addEventListener("click", retry, {once: true})
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportMapUnavailable = (element, reason, error = null) => {
|
||||||
|
element.dataset.mapUnavailable = "true"
|
||||||
|
element.dataset.mapUnavailableReason = reason
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.warn(`[Who Need Help] Map unavailable (${reason})`, error)
|
||||||
|
} else {
|
||||||
|
console.warn(`[Who Need Help] Map unavailable (${reason})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createAidMap = element => {
|
const createAidMap = element => {
|
||||||
const state = {
|
const state = {
|
||||||
element,
|
element,
|
||||||
|
|
@ -154,7 +175,8 @@ const createAidMap = element => {
|
||||||
areaSelectedId: `${element.id || "aid-map"}-area-selected`,
|
areaSelectedId: `${element.id || "aid-map"}-area-selected`,
|
||||||
clusterSourceId: `${element.id || "aid-map"}-clusters`,
|
clusterSourceId: `${element.id || "aid-map"}-clusters`,
|
||||||
clusterCircleId: `${element.id || "aid-map"}-cluster-circle`,
|
clusterCircleId: `${element.id || "aid-map"}-cluster-circle`,
|
||||||
clusterCountId: `${element.id || "aid-map"}-cluster-count`
|
clusterCountId: `${element.id || "aid-map"}-cluster-count`,
|
||||||
|
statusBadge: element.parentElement?.querySelector("[data-map-status-badge]") || null
|
||||||
}
|
}
|
||||||
|
|
||||||
state.markLoading = () => {
|
state.markLoading = () => {
|
||||||
|
|
@ -167,16 +189,33 @@ const createAidMap = element => {
|
||||||
state.element.dataset.mapReady = "true"
|
state.element.dataset.mapReady = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
state.showUnavailable = () => {
|
state.retry = () => {
|
||||||
|
if (!state.active) return
|
||||||
|
|
||||||
|
state.element.dataset.mapReady = "false"
|
||||||
|
delete state.element.dataset.mapUnavailable
|
||||||
|
delete state.element.dataset.mapUnavailableReason
|
||||||
|
state.element.replaceChildren()
|
||||||
|
state.mount()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.showUnavailable = (reason, error = null) => {
|
||||||
state.map = null
|
state.map = null
|
||||||
state.element.dataset.mapUnavailable = "true"
|
reportMapUnavailable(state.element, reason, error)
|
||||||
state.markReady()
|
state.markReady()
|
||||||
|
|
||||||
|
if (state.statusBadge) {
|
||||||
|
state.statusBadge.textContent = state.statusBadge.dataset.staticLabel
|
||||||
|
}
|
||||||
|
|
||||||
if (state.element.dataset.demoMap !== "true") {
|
if (state.element.dataset.demoMap !== "true") {
|
||||||
const fallback = document.createElement("p")
|
const fallback = document.createElement("div")
|
||||||
fallback.className = "grid h-full place-items-center p-6 text-center text-sm"
|
fallback.className = "map-unavailable-panel"
|
||||||
fallback.dataset.mapFallback = "true"
|
fallback.dataset.mapFallback = "true"
|
||||||
fallback.textContent = state.element.dataset.mapUnavailableLabel
|
|
||||||
|
const message = document.createElement("p")
|
||||||
|
message.textContent = state.element.dataset.mapUnavailableLabel
|
||||||
|
fallback.append(message, mapRetryButton(state.element, state.retry))
|
||||||
state.element.replaceChildren(fallback)
|
state.element.replaceChildren(fallback)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +255,7 @@ const createAidMap = element => {
|
||||||
points.append(item)
|
points.append(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
fallback.append(points)
|
fallback.append(points, mapRetryButton(state.element, state.retry))
|
||||||
state.element.replaceChildren(fallback)
|
state.element.replaceChildren(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,8 +440,10 @@ const createAidMap = element => {
|
||||||
state.markLoading()
|
state.markLoading()
|
||||||
const demoMap = state.element.dataset.demoMap === "true"
|
const demoMap = state.element.dataset.demoMap === "true"
|
||||||
|
|
||||||
if (!supportsMapCanvas()) {
|
const support = mapCanvasSupport()
|
||||||
state.showUnavailable()
|
|
||||||
|
if (!support.supported) {
|
||||||
|
state.showUnavailable(support.reason, support.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,14 +455,13 @@ const createAidMap = element => {
|
||||||
zoom: 5,
|
zoom: 5,
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
renderWorldCopies: false,
|
renderWorldCopies: false,
|
||||||
maxBounds: [[-180, -85.05112878], [180, 85.05112878]],
|
|
||||||
trackResize: false,
|
trackResize: false,
|
||||||
dragRotate: !demoMap,
|
dragRotate: !demoMap,
|
||||||
pitchWithRotate: !demoMap,
|
pitchWithRotate: !demoMap,
|
||||||
scrollZoom: !demoMap
|
scrollZoom: !demoMap
|
||||||
})
|
})
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
state.showUnavailable()
|
state.showUnavailable("initialization-failed", error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,7 +487,14 @@ const createAidMap = element => {
|
||||||
state.onLoad = () => {
|
state.onLoad = () => {
|
||||||
if (!state.active || !state.map) return
|
if (!state.active || !state.map) return
|
||||||
|
|
||||||
|
delete state.element.dataset.mapUnavailable
|
||||||
|
delete state.element.dataset.mapUnavailableReason
|
||||||
state.markReady()
|
state.markReady()
|
||||||
|
|
||||||
|
if (state.statusBadge) {
|
||||||
|
state.statusBadge.textContent = state.statusBadge.dataset.interactiveLabel
|
||||||
|
}
|
||||||
|
|
||||||
state.applySavedViewport()
|
state.applySavedViewport()
|
||||||
state.renderMarkers()
|
state.renderMarkers()
|
||||||
|
|
||||||
|
|
@ -913,11 +960,35 @@ export const Hooks = {
|
||||||
this.statusElement = this.el.querySelector(this.el.dataset.statusTarget)
|
this.statusElement = this.el.querySelector(this.el.dataset.statusTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderMapUnavailable = () => {
|
this.retryMap = () => {
|
||||||
if (!this.mapElement) return
|
if (!this.mapElement) return
|
||||||
|
|
||||||
this.mapElement.dataset.mapUnavailable = "true"
|
this.mapUnavailable = false
|
||||||
this.mapElement.textContent = this.mapElement.dataset.mapUnavailableLabel
|
delete this.el.dataset.mapUnavailable
|
||||||
|
this.mapElement.dataset.mapReady = "false"
|
||||||
|
delete this.mapElement.dataset.mapUnavailable
|
||||||
|
delete this.mapElement.dataset.mapUnavailableReason
|
||||||
|
this.mapElement.replaceChildren()
|
||||||
|
this.mountMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderMapUnavailable = (reason = "webgl-context-unavailable", error = null) => {
|
||||||
|
if (!this.mapElement) return
|
||||||
|
|
||||||
|
this.mapUnavailableReason = reason
|
||||||
|
this.mapUnavailableError = error
|
||||||
|
this.el.dataset.mapUnavailable = "true"
|
||||||
|
reportMapUnavailable(this.mapElement, reason, error)
|
||||||
|
this.mapElement.dataset.mapReady = "true"
|
||||||
|
|
||||||
|
const fallback = document.createElement("div")
|
||||||
|
fallback.className = "map-unavailable-panel"
|
||||||
|
fallback.dataset.mapFallback = "true"
|
||||||
|
|
||||||
|
const message = document.createElement("p")
|
||||||
|
message.textContent = this.mapElement.dataset.mapUnavailableLabel
|
||||||
|
fallback.append(message, mapRetryButton(this.mapElement, this.retryMap))
|
||||||
|
this.mapElement.replaceChildren(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mode = () =>
|
this.mode = () =>
|
||||||
|
|
@ -1199,56 +1270,84 @@ export const Hooks = {
|
||||||
this.el.addEventListener("change", this.onChange)
|
this.el.addEventListener("change", this.onChange)
|
||||||
this.el.addEventListener("click", this.onClick)
|
this.el.addEventListener("click", this.onClick)
|
||||||
|
|
||||||
if (!this.mapElement || !supportsMapCanvas()) {
|
this.mountMap = () => {
|
||||||
if (this.mapElement) {
|
if (!this.mapElement) return
|
||||||
|
|
||||||
|
const support = mapCanvasSupport()
|
||||||
|
|
||||||
|
if (!support.supported) {
|
||||||
this.mapUnavailable = true
|
this.mapUnavailable = true
|
||||||
this.renderMapUnavailable()
|
this.renderMapUnavailable(support.reason, support.error)
|
||||||
|
this.syncMode()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialCoordinates = this.coordinates()
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.map = new maplibregl.Map({
|
||||||
|
container: this.mapElement,
|
||||||
|
style: defaultStyle,
|
||||||
|
center: initialCoordinates
|
||||||
|
? [initialCoordinates.longitude, initialCoordinates.latitude]
|
||||||
|
: [30.5234, 50.4501],
|
||||||
|
zoom: initialCoordinates ? 13 : 5,
|
||||||
|
attributionControl: true,
|
||||||
|
dragRotate: false,
|
||||||
|
pitchWithRotate: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.map = null
|
||||||
|
this.mapUnavailable = true
|
||||||
|
this.renderMapUnavailable("initialization-failed", error)
|
||||||
|
this.syncMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.touchZoomRotate.disableRotation()
|
||||||
|
this.map.addControl(new maplibregl.NavigationControl({showCompass: false}), "top-left")
|
||||||
|
|
||||||
|
this.map.on("load", () => {
|
||||||
|
this.mapLoaded = true
|
||||||
|
this.mapUnavailable = false
|
||||||
|
delete this.el.dataset.mapUnavailable
|
||||||
|
this.mapElement.dataset.mapReady = "true"
|
||||||
|
delete this.mapElement.dataset.mapUnavailable
|
||||||
|
delete this.mapElement.dataset.mapUnavailableReason
|
||||||
|
this.ensureAreaLayers()
|
||||||
|
this.renderSelection()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.on("click", event => {
|
||||||
|
if (this.mode() === "hidden") return
|
||||||
|
|
||||||
|
this.setCoordinates(
|
||||||
|
{latitude: event.lngLat.lat, longitude: event.lngLat.lng},
|
||||||
|
{center: true}
|
||||||
|
)
|
||||||
|
this.setStatus(
|
||||||
|
this.mode() === "approximate_public"
|
||||||
|
? this.el.dataset.areaSelectedLabel
|
||||||
|
: this.el.dataset.pointSelectedLabel
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
this.syncMode()
|
this.syncMode()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialCoordinates = this.coordinates()
|
this.mountMap()
|
||||||
this.map = new maplibregl.Map({
|
|
||||||
container: this.mapElement,
|
|
||||||
style: defaultStyle,
|
|
||||||
center: initialCoordinates
|
|
||||||
? [initialCoordinates.longitude, initialCoordinates.latitude]
|
|
||||||
: [30.5234, 50.4501],
|
|
||||||
zoom: initialCoordinates ? 13 : 5,
|
|
||||||
attributionControl: true,
|
|
||||||
dragRotate: false,
|
|
||||||
pitchWithRotate: false
|
|
||||||
})
|
|
||||||
this.map.touchZoomRotate.disableRotation()
|
|
||||||
this.map.addControl(new maplibregl.NavigationControl({showCompass: false}), "top-left")
|
|
||||||
|
|
||||||
this.map.on("load", () => {
|
|
||||||
this.mapLoaded = true
|
|
||||||
this.mapElement.dataset.mapReady = "true"
|
|
||||||
this.ensureAreaLayers()
|
|
||||||
this.renderSelection()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.map.on("click", event => {
|
|
||||||
if (this.mode() === "hidden") return
|
|
||||||
|
|
||||||
this.setCoordinates(
|
|
||||||
{latitude: event.lngLat.lat, longitude: event.lngLat.lng},
|
|
||||||
{center: true}
|
|
||||||
)
|
|
||||||
this.setStatus(
|
|
||||||
this.mode() === "approximate_public"
|
|
||||||
? this.el.dataset.areaSelectedLabel
|
|
||||||
: this.el.dataset.pointSelectedLabel
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.syncMode()
|
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
this.cacheElements()
|
this.cacheElements()
|
||||||
if (this.mapUnavailable) this.renderMapUnavailable()
|
|
||||||
|
if (this.mapUnavailable) {
|
||||||
|
this.el.dataset.mapUnavailable = "true"
|
||||||
|
|
||||||
|
if (!this.mapElement?.querySelector("[data-map-fallback=true]")) {
|
||||||
|
this.renderMapUnavailable(this.mapUnavailableReason, this.mapUnavailableError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.syncMode()
|
this.syncMode()
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
|
|
@ -1367,7 +1466,11 @@ export const Hooks = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function supportsMapCanvas() {
|
function mapCanvasSupport() {
|
||||||
|
if (!window.WebGLRenderingContext) {
|
||||||
|
return {supported: false, reason: "webgl-api-unavailable"}
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = document.createElement("canvas")
|
const canvas = document.createElement("canvas")
|
||||||
const attributes = {
|
const attributes = {
|
||||||
alpha: true,
|
alpha: true,
|
||||||
|
|
@ -1381,10 +1484,13 @@ function supportsMapCanvas() {
|
||||||
canvas.getContext("webgl2", attributes) ||
|
canvas.getContext("webgl2", attributes) ||
|
||||||
canvas.getContext("webgl", attributes)
|
canvas.getContext("webgl", attributes)
|
||||||
|
|
||||||
if (!context) return false
|
if (!context || typeof context.getParameter !== "function") {
|
||||||
|
return {supported: false, reason: "webgl-context-unavailable"}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
context.getExtension("WEBGL_lose_context")?.loseContext()
|
||||||
} catch (_error) {
|
return {supported: true}
|
||||||
return false
|
} catch (error) {
|
||||||
|
return {supported: false, reason: "webgl-context-disabled", error}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
services:
|
services:
|
||||||
migrate:
|
migrate:
|
||||||
image: who-need-help:e2e
|
image: ${APP_IMAGE:?Set APP_IMAGE for the isolated E2E run}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
build:
|
build:
|
||||||
args:
|
args:
|
||||||
WNH_E2E_ROUTES: "true"
|
WNH_E2E_ROUTES: "true"
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: who-need-help:e2e
|
image: ${APP_IMAGE:?Set APP_IMAGE for the isolated E2E run}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
labels:
|
labels:
|
||||||
- traefik.http.routers.${TRAEFIK_APP_NAME}-tls.rule=${TRAEFIK_ROUTER_RULE}
|
- traefik.http.routers.${TRAEFIK_APP_NAME}-tls.rule=${TRAEFIK_ROUTER_RULE}
|
||||||
|
|
@ -17,11 +17,11 @@ services:
|
||||||
- traefik.http.routers.${TRAEFIK_APP_NAME}-tls.tls=true
|
- traefik.http.routers.${TRAEFIK_APP_NAME}-tls.tls=true
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
image: who-need-help:e2e
|
image: ${APP_IMAGE:?Set APP_IMAGE for the isolated E2E run}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
image: who-need-help-e2e-tests:local
|
image: ${E2E_TEST_IMAGE:?Set E2E_TEST_IMAGE for the isolated E2E run}
|
||||||
build:
|
build:
|
||||||
context: e2e
|
context: e2e
|
||||||
init: true
|
init: true
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,16 @@ export function captureBrowserFailures(
|
||||||
|
|
||||||
page.on("console", (message) => {
|
page.on("console", (message) => {
|
||||||
if (message.type() === "error") {
|
if (message.type() === "error") {
|
||||||
failures.push(`console: ${message.text()}`);
|
const text = message.text();
|
||||||
|
const abortedE2eRasterTile =
|
||||||
|
text.includes("AJAXError: Failed to fetch (0):") &&
|
||||||
|
text.includes("/__e2e__/map-tile.png");
|
||||||
|
|
||||||
|
// Chromium reports an aborted MapLibre worker fetch as both a console
|
||||||
|
// error and a request failure when navigation destroys the old map.
|
||||||
|
// requestfailed below remains authoritative: it ignores only explicit
|
||||||
|
// browser cancellation and still records genuine tile failures.
|
||||||
|
if (!abortedE2eRasterTile) failures.push(`console: ${text}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,48 @@ test("public safety pages load and private discovery requires authentication", a
|
||||||
|
|
||||||
assertNoBrowserFailures();
|
assertNoBrowserFailures();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("public demo labels the non-WebGL fallback honestly and offers retry", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const assertNoBrowserFailures = captureBrowserFailures(page);
|
||||||
|
const mapWarnings: string[] = [];
|
||||||
|
|
||||||
|
page.on("console", (message) => {
|
||||||
|
if (message.type() === "warning" && message.text().includes("Map unavailable")) {
|
||||||
|
mapWarnings.push(message.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
||||||
|
|
||||||
|
HTMLCanvasElement.prototype.getContext = function (
|
||||||
|
contextId: string,
|
||||||
|
options?: unknown,
|
||||||
|
) {
|
||||||
|
if (contextId === "webgl" || contextId === "webgl2") return null;
|
||||||
|
return originalGetContext.call(this, contextId, options as never);
|
||||||
|
} as typeof HTMLCanvasElement.prototype.getContext;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const map = page.locator("#home-demo-map");
|
||||||
|
const status = page.locator("[data-map-status-badge]");
|
||||||
|
const retry = map.getByRole("button", { name: "Try map again" });
|
||||||
|
|
||||||
|
await map.scrollIntoViewIfNeeded();
|
||||||
|
await expect(map.locator("[data-map-fallback=true]")).toBeVisible();
|
||||||
|
await expect(status).toHaveText("Static preview");
|
||||||
|
await expect(retry).toBeVisible();
|
||||||
|
await expect(map).toHaveAttribute("data-map-unavailable-reason", "webgl-context-unavailable");
|
||||||
|
await expect.poll(() => mapWarnings.length).toBe(1);
|
||||||
|
|
||||||
|
await retry.click();
|
||||||
|
await expect.poll(() => mapWarnings.length).toBe(2);
|
||||||
|
await expect(status).toHaveText("Static preview");
|
||||||
|
await expect(retry).toBeVisible();
|
||||||
|
|
||||||
|
assertNoBrowserFailures();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -147,15 +147,26 @@
|
||||||
data-map-ready="false"
|
data-map-ready="false"
|
||||||
data-markers={demo_markers}
|
data-markers={demo_markers}
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The demo map is unavailable. The example steps remain available beside it.")
|
gettext(
|
||||||
|
"The interactive demo could not start in this browser. You can retry or use the example steps beside it."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={gettext("Demo map with synthetic approximate help requests")}
|
aria-label={gettext("Demo map with synthetic approximate help requests")}
|
||||||
class="aid-map home-demo-map"
|
class="aid-map home-demo-map"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="pointer-events-none absolute left-4 top-4 z-10 flex flex-wrap gap-2">
|
<div class="pointer-events-none absolute left-4 top-4 z-10 flex flex-wrap gap-2">
|
||||||
<span class="badge badge-neutral shadow-sm">{gettext("Interactive demo")}</span>
|
<span
|
||||||
|
data-map-status-badge
|
||||||
|
data-interactive-label={gettext("Interactive demo")}
|
||||||
|
data-static-label={gettext("Static preview")}
|
||||||
|
aria-live="polite"
|
||||||
|
class="badge badge-neutral shadow-sm"
|
||||||
|
>
|
||||||
|
{gettext("Map preview")}
|
||||||
|
</span>
|
||||||
<span class="badge badge-ghost border border-base-300 bg-base-100/95 shadow-sm">
|
<span class="badge badge-ghost border border-base-300 bg-base-100/95 shadow-sm">
|
||||||
{gettext("Example data — not live requests")}
|
{gettext("Example data — not live requests")}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -539,6 +539,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
<div class="rounded-2xl bg-base-200 p-5 text-sm">
|
<div class="rounded-2xl bg-base-200 p-5 text-sm">
|
||||||
|
|
|
||||||
|
|
@ -514,6 +514,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
data-request-label={gettext("Open request")}
|
data-request-label={gettext("Open request")}
|
||||||
class="aid-map request-discovery-map"
|
class="aid-map request-discovery-map"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
|
||||||
data-point-marker-label={gettext("Drag to move the exact point")}
|
data-point-marker-label={gettext("Drag to move the exact point")}
|
||||||
data-hidden-label={gettext("No map coordinates will be stored.")}
|
data-hidden-label={gettext("No map coordinates will be stored.")}
|
||||||
data-location-unavailable-label={gettext("Location is unavailable in this browser.")}
|
data-location-unavailable-label={gettext("Location is unavailable in this browser.")}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
data-location-denied-label={gettext("Location permission was denied.")}
|
data-location-denied-label={gettext("Location permission was denied.")}
|
||||||
class="request-location-workspace"
|
class="request-location-workspace"
|
||||||
>
|
>
|
||||||
|
|
@ -462,6 +463,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The map is unavailable. You can use the manual coordinate fallback.")
|
gettext("The map is unavailable. You can use the manual coordinate fallback.")
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
class="aid-map request-location-map"
|
class="aid-map request-location-map"
|
||||||
/>
|
/>
|
||||||
<div class="request-location-map-hint">
|
<div class="request-location-map-hint">
|
||||||
|
|
|
||||||
|
|
@ -1045,6 +1045,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
|
||||||
data-map-unavailable-label={
|
data-map-unavailable-label={
|
||||||
gettext("The map is unavailable in this browser. Request details remain usable.")
|
gettext("The map is unavailable in this browser. Request details remain usable.")
|
||||||
}
|
}
|
||||||
|
data-map-retry-label={gettext("Try map again")}
|
||||||
class="aid-map"
|
class="aid-map"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -477,7 +477,7 @@ cleanup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker rm -f "$container" >/dev/null 2>&1 || true
|
docker rm -f "$container" >/dev/null 2>&1 || true
|
||||||
docker volume rm -f "$avd_volume" >/dev/null 2>&1 || true
|
docker volume rm "$avd_volume" >/dev/null 2>&1 || true
|
||||||
if [[ -n "$remote_target" ]]; then
|
if [[ -n "$remote_target" ]]; then
|
||||||
ssh "$remote_host" bash -s -- \
|
ssh "$remote_host" bash -s -- \
|
||||||
"$remote_root" "$remote_output_dir" "$remote_tools_image" \
|
"$remote_root" "$remote_output_dir" "$remote_tools_image" \
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ mkdir -p "$output"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
status=$?
|
status=$?
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
if [ -n "$geo_pid" ]; then
|
if [ -n "$geo_pid" ]; then
|
||||||
kill "$geo_pid" >/dev/null 2>&1 || true
|
kill "$geo_pid" >/dev/null 2>&1 || true
|
||||||
|
|
@ -115,7 +116,7 @@ cleanup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker rm -f "$container" >/dev/null 2>&1 || true
|
docker rm -f "$container" >/dev/null 2>&1 || true
|
||||||
docker volume rm -f "$avd_volume" >/dev/null 2>&1 || true
|
docker volume rm "$avd_volume" >/dev/null 2>&1 || true
|
||||||
docker image rm "$image" >/dev/null 2>&1 || true
|
docker image rm "$image" >/dev/null 2>&1 || true
|
||||||
exit "$status"
|
exit "$status"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ cleanup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker rm -f "$container" >/dev/null 2>&1 || true
|
docker rm -f "$container" >/dev/null 2>&1 || true
|
||||||
docker volume rm -f "$avd_volume" >/dev/null 2>&1 || true
|
docker volume rm "$avd_volume" >/dev/null 2>&1 || true
|
||||||
docker image rm "$image" >/dev/null 2>&1 || true
|
docker image rm "$image" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,13 @@ mkdir -p "$output_dir"
|
||||||
export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
||||||
export TRAEFIK_APP_NAME="who-need-help-e2e-$run_id"
|
export TRAEFIK_APP_NAME="who-need-help-e2e-$run_id"
|
||||||
export TRAEFIK_DOCKER_NETWORK="${project}_ingress"
|
export TRAEFIK_DOCKER_NETWORK="${project}_ingress"
|
||||||
# Compose interpolates required base-image variables before applying the E2E
|
# Each run owns its image tags. The cleanup trap removes only these exact tags
|
||||||
# override. Keep those names local to the isolated runner; the application
|
# after their project containers are gone, so parallel or interrupted runs do
|
||||||
# services themselves are still replaced with `who-need-help:e2e` below.
|
# not accumulate shared test tags.
|
||||||
export APP_IMAGE="who-need-help:e2e"
|
export APP_IMAGE="who-need-help:e2e-$run_id"
|
||||||
export SOCKET_PROXY_IMAGE="who-need-help:socket-proxy-e2e"
|
export SOCKET_PROXY_IMAGE="who-need-help:socket-proxy-e2e-$run_id"
|
||||||
export POSTGIS_IMAGE="who-need-help:postgis-e2e"
|
export POSTGIS_IMAGE="who-need-help:postgis-e2e-$run_id"
|
||||||
|
export E2E_TEST_IMAGE="who-need-help-e2e-tests:$run_id"
|
||||||
export E2E_OUTPUT_DIR="$output_dir"
|
export E2E_OUTPUT_DIR="$output_dir"
|
||||||
export E2E_UID
|
export E2E_UID
|
||||||
E2E_UID=$(id -u)
|
E2E_UID=$(id -u)
|
||||||
|
|
@ -38,7 +39,7 @@ export CODEX_SESSION_ID="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}}"
|
||||||
# Keep raster-map traffic inside the isolated application network. MapLibre
|
# Keep raster-map traffic inside the isolated application network. MapLibre
|
||||||
# performs tile fetches from a worker, so browser-context request interception
|
# performs tile fetches from a worker, so browser-context request interception
|
||||||
# is not a reliable network boundary for these requests.
|
# is not a reliable network boundary for these requests.
|
||||||
export MAP_TILE_URL="https://proxy/__e2e__/map-tile.png?z={z}&x={x}&y={y}"
|
export MAP_TILE_URL="/__e2e__/map-tile.png?z={z}&x={x}&y={y}"
|
||||||
|
|
||||||
compose() {
|
compose() {
|
||||||
docker compose \
|
docker compose \
|
||||||
|
|
@ -49,8 +50,36 @@ compose() {
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
image_tag_is_referenced() {
|
||||||
|
expected_image=$1
|
||||||
|
|
||||||
|
for container_id in $(docker ps -aq --filter "ancestor=$expected_image"); do
|
||||||
|
observed_image=$(docker inspect --format '{{.Config.Image}}' "$container_id")
|
||||||
|
if [ "$observed_image" = "$expected_image" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_run_image() {
|
||||||
|
image=$1
|
||||||
|
|
||||||
|
if docker image inspect "$image" >/dev/null 2>&1; then
|
||||||
|
if image_tag_is_referenced "$image"; then
|
||||||
|
echo "Refusing to remove referenced E2E image: $image" >&2
|
||||||
|
cleanup_status=1
|
||||||
|
else
|
||||||
|
docker image rm "$image" >/dev/null || cleanup_status=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
status=$?
|
status=$?
|
||||||
|
cleanup_status=0
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
if [ "$status" -ne 0 ]; then
|
if [ "$status" -ne 0 ]; then
|
||||||
compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true
|
compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true
|
||||||
|
|
@ -61,7 +90,35 @@ cleanup() {
|
||||||
echo "Keeping isolated E2E project $project for inspection."
|
echo "Keeping isolated E2E project $project for inspection."
|
||||||
echo "Artifacts: $output_dir"
|
echo "Artifacts: $output_dir"
|
||||||
else
|
else
|
||||||
compose down --volumes --remove-orphans >/dev/null
|
compose down --remove-orphans >/dev/null 2>&1 || cleanup_status=$?
|
||||||
|
|
||||||
|
postgres_volume="${project}_postgres_data"
|
||||||
|
|
||||||
|
if docker volume inspect "$postgres_volume" >/dev/null 2>&1; then
|
||||||
|
observed_project=$(docker volume inspect --format \
|
||||||
|
'{{index .Labels "com.docker.compose.project"}}' "$postgres_volume")
|
||||||
|
observed_volume=$(docker volume inspect --format \
|
||||||
|
'{{index .Labels "com.docker.compose.volume"}}' "$postgres_volume")
|
||||||
|
remaining_refs=$(docker ps -aq --filter "volume=$postgres_volume")
|
||||||
|
|
||||||
|
if [ "$observed_project" != "$project" ] || \
|
||||||
|
[ "$observed_volume" != "postgres_data" ] || \
|
||||||
|
[ -n "$remaining_refs" ]; then
|
||||||
|
echo "Refusing to remove unexpected or referenced E2E volume: $postgres_volume" >&2
|
||||||
|
cleanup_status=1
|
||||||
|
else
|
||||||
|
docker volume rm "$postgres_volume" >/dev/null || cleanup_status=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
remove_run_image "$E2E_TEST_IMAGE"
|
||||||
|
remove_run_image "$APP_IMAGE"
|
||||||
|
remove_run_image "$POSTGIS_IMAGE"
|
||||||
|
remove_run_image "$SOCKET_PROXY_IMAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then
|
||||||
|
status=$cleanup_status
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit "$status"
|
exit "$status"
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,15 @@ case "$project" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
run_id=${project#who_need_help_e2e_}
|
||||||
|
|
||||||
# Compose still resolves required interpolation values for `down`. These values
|
# Compose still resolves required interpolation values for `down`. These values
|
||||||
# mirror the isolated runner and do not select containers; the project label
|
# mirror the exact per-run tags. The project label passed above remains the
|
||||||
# passed above is the scope used by Compose for removal.
|
# Compose resource scope.
|
||||||
export APP_IMAGE="who-need-help:e2e"
|
export APP_IMAGE="who-need-help:e2e-$run_id"
|
||||||
export SOCKET_PROXY_IMAGE="who-need-help:socket-proxy-e2e"
|
export SOCKET_PROXY_IMAGE="who-need-help:socket-proxy-e2e-$run_id"
|
||||||
export POSTGIS_IMAGE="who-need-help:postgis-e2e"
|
export POSTGIS_IMAGE="who-need-help:postgis-e2e-$run_id"
|
||||||
|
export E2E_TEST_IMAGE="who-need-help-e2e-tests:$run_id"
|
||||||
export E2E_OUTPUT_DIR="$ROOT/output/e2e/stop-placeholder"
|
export E2E_OUTPUT_DIR="$ROOT/output/e2e/stop-placeholder"
|
||||||
export E2E_UID
|
export E2E_UID
|
||||||
E2E_UID=$(id -u)
|
E2E_UID=$(id -u)
|
||||||
|
|
@ -32,11 +34,51 @@ E2E_GID=$(id -g)
|
||||||
export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
export TRAEFIK_PROJECT_CONSTRAINT="$project"
|
||||||
export TRAEFIK_APP_NAME="$project"
|
export TRAEFIK_APP_NAME="$project"
|
||||||
export TRAEFIK_DOCKER_NETWORK="${project}_ingress"
|
export TRAEFIK_DOCKER_NETWORK="${project}_ingress"
|
||||||
export MAP_TILE_URL="https://proxy/__e2e__/map-tile.png?z={z}&x={x}&y={y}"
|
export MAP_TILE_URL="/__e2e__/map-tile.png?z={z}&x={x}&y={y}"
|
||||||
|
|
||||||
docker compose \
|
docker compose \
|
||||||
--project-name "$project" \
|
--project-name "$project" \
|
||||||
--env-file "$ROOT/.env.e2e" \
|
--env-file "$ROOT/.env.e2e" \
|
||||||
--file "$ROOT/compose.yaml" \
|
--file "$ROOT/compose.yaml" \
|
||||||
--file "$ROOT/compose.e2e.yaml" \
|
--file "$ROOT/compose.e2e.yaml" \
|
||||||
down --volumes --remove-orphans
|
down --remove-orphans
|
||||||
|
|
||||||
|
postgres_volume="${project}_postgres_data"
|
||||||
|
|
||||||
|
if docker volume inspect "$postgres_volume" >/dev/null 2>&1; then
|
||||||
|
observed_project=$(docker volume inspect --format \
|
||||||
|
'{{index .Labels "com.docker.compose.project"}}' "$postgres_volume")
|
||||||
|
observed_volume=$(docker volume inspect --format \
|
||||||
|
'{{index .Labels "com.docker.compose.volume"}}' "$postgres_volume")
|
||||||
|
remaining_refs=$(docker ps -aq --filter "volume=$postgres_volume")
|
||||||
|
|
||||||
|
if [ "$observed_project" != "$project" ] || \
|
||||||
|
[ "$observed_volume" != "postgres_data" ] || \
|
||||||
|
[ -n "$remaining_refs" ]; then
|
||||||
|
echo "Refusing to remove unexpected or referenced E2E volume: $postgres_volume" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker volume rm "$postgres_volume"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for image in "$E2E_TEST_IMAGE" "$APP_IMAGE" "$POSTGIS_IMAGE" "$SOCKET_PROXY_IMAGE"; do
|
||||||
|
if docker image inspect "$image" >/dev/null 2>&1; then
|
||||||
|
referenced=false
|
||||||
|
|
||||||
|
for container_id in $(docker ps -aq --filter "ancestor=$image"); do
|
||||||
|
observed_image=$(docker inspect --format '{{.Config.Image}}' "$container_id")
|
||||||
|
if [ "$observed_image" = "$image" ]; then
|
||||||
|
referenced=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$referenced" = true ]; then
|
||||||
|
echo "Refusing to remove referenced E2E image: $image" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker image rm "$image"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,13 @@ if [ -f "$target" ]; then
|
||||||
updated=true
|
updated=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if grep -Eq '^MAP_TILE_URL=https?://proxy/__e2e__/map-tile\.png' "$target"; then
|
||||||
|
sed -i \
|
||||||
|
's|^MAP_TILE_URL=https\?://proxy/__e2e__/map-tile\.png|MAP_TILE_URL=/__e2e__/map-tile.png|' \
|
||||||
|
"$target"
|
||||||
|
updated=true
|
||||||
|
fi
|
||||||
|
|
||||||
chmod 600 "$target"
|
chmod 600 "$target"
|
||||||
|
|
||||||
if [ "$updated" = true ]; then
|
if [ "$updated" = true ]; then
|
||||||
|
|
@ -59,7 +66,7 @@ TRAEFIK_ROUTER_RULE='PathPrefix(\`/\`)'
|
||||||
PHX_HOST=proxy
|
PHX_HOST=proxy
|
||||||
PHX_SCHEME=https
|
PHX_SCHEME=https
|
||||||
PHX_URL_PORT=443
|
PHX_URL_PORT=443
|
||||||
MAP_TILE_URL=https://proxy/__e2e__/map-tile.png?z={z}&x={x}&y={y}
|
MAP_TILE_URL=/__e2e__/map-tile.png?z={z}&x={x}&y={y}
|
||||||
POSTGRES_DB=who_need_help_e2e
|
POSTGRES_DB=who_need_help_e2e
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=$postgres_password
|
POSTGRES_PASSWORD=$postgres_password
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,29 @@ docker compose \
|
||||||
-p "$LOAD_PROJECT" \
|
-p "$LOAD_PROJECT" \
|
||||||
-f compose.yaml \
|
-f compose.yaml \
|
||||||
-f compose.load.yaml \
|
-f compose.load.yaml \
|
||||||
stop
|
down --remove-orphans
|
||||||
|
|
||||||
echo "Stopped the isolated load-profile containers; its database volume remains."
|
# Optional observability and backup overlays create additional networks which
|
||||||
|
# are not present in the base load Compose file. Remove only unattached
|
||||||
|
# networks carrying this exact isolated project label.
|
||||||
|
for network_id in $(docker network ls -q \
|
||||||
|
--filter "label=com.docker.compose.project=$LOAD_PROJECT"); do
|
||||||
|
observed_project=$(docker network inspect --format \
|
||||||
|
'{{index .Labels "com.docker.compose.project"}}' "$network_id")
|
||||||
|
endpoints=$(docker network inspect --format '{{len .Containers}}' "$network_id")
|
||||||
|
|
||||||
|
if [ "$observed_project" != "$LOAD_PROJECT" ] || [ "$endpoints" != 0 ]; then
|
||||||
|
echo "Refusing unexpected or connected load-profile network: $network_id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm "$network_id" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
if image_id=$(docker image inspect --format '{{.Id}}' who-need-help:load-tools 2>/dev/null); then
|
||||||
|
if [ -z "$(docker ps -aq --filter "ancestor=$image_id")" ]; then
|
||||||
|
docker image rm who-need-help:load-tools >/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removed the isolated load-profile containers and networks; its named volumes remain."
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,60 @@ case "$LOAD_WEB_REPLICAS:$LOAD_WORKER_REPLICAS" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
compose() {
|
||||||
|
docker compose \
|
||||||
|
--env-file "$ENV_FILE" \
|
||||||
|
-p "$LOAD_PROJECT" \
|
||||||
|
-f compose.yaml \
|
||||||
|
-f compose.load.yaml \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_failed_start() {
|
||||||
|
status=$?
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
|
# A cancelled Docker API request can finish creating a network just after
|
||||||
|
# the first `compose down`. Repeat the exact project-scoped teardown until
|
||||||
|
# the project has no containers or networks, or the bounded retry expires.
|
||||||
|
attempt=0
|
||||||
|
while [ "$attempt" -lt 5 ]; do
|
||||||
|
compose down --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
remaining_containers=$(docker ps -aq \
|
||||||
|
--filter "label=com.docker.compose.project=$LOAD_PROJECT")
|
||||||
|
remaining_networks=$(docker network ls -q \
|
||||||
|
--filter "label=com.docker.compose.project=$LOAD_PROJECT")
|
||||||
|
|
||||||
|
if [ -z "$remaining_containers" ] && [ -z "$remaining_networks" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if image_id=$(docker image inspect --format '{{.Id}}' who-need-help:load-tools 2>/dev/null); then
|
||||||
|
if [ -z "$(docker ps -aq --filter "ancestor=$image_id")" ]; then
|
||||||
|
docker image rm who-need-help:load-tools >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$(docker ps -aq --filter "label=com.docker.compose.project=$LOAD_PROJECT")" ] || \
|
||||||
|
[ -n "$(docker network ls -q --filter "label=com.docker.compose.project=$LOAD_PROJECT")" ]; then
|
||||||
|
echo "Load-profile startup cleanup left project resources behind." >&2
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_failed_start EXIT HUP INT TERM
|
||||||
|
|
||||||
docker build --target load_tools --tag who-need-help:load-tools .
|
docker build --target load_tools --tag who-need-help:load-tools .
|
||||||
|
|
||||||
docker compose \
|
compose up -d --build --wait \
|
||||||
--env-file "$ENV_FILE" \
|
|
||||||
-p "$LOAD_PROJECT" \
|
|
||||||
-f compose.yaml \
|
|
||||||
-f compose.load.yaml \
|
|
||||||
up -d --build --wait \
|
|
||||||
--scale "web=$LOAD_WEB_REPLICAS" \
|
--scale "web=$LOAD_WEB_REPLICAS" \
|
||||||
--scale "worker=$LOAD_WORKER_REPLICAS"
|
--scale "worker=$LOAD_WORKER_REPLICAS"
|
||||||
|
|
||||||
|
|
@ -65,3 +111,5 @@ curl --fail --silent --show-error \
|
||||||
"http://localhost:$HTTP_PORT/healthz/ready"
|
"http://localhost:$HTTP_PORT/healthz/ready"
|
||||||
printf '\nLoad profile is ready with %s web and %s worker replicas.\n' \
|
printf '\nLoad profile is ready with %s web and %s worker replicas.\n' \
|
||||||
"$LOAD_WEB_REPLICAS" "$LOAD_WORKER_REPLICAS"
|
"$LOAD_WEB_REPLICAS" "$LOAD_WORKER_REPLICAS"
|
||||||
|
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export QUALITY_POSTGIS_IMAGE
|
||||||
compose="docker compose -p $project -f $ROOT/compose.quality.yaml"
|
compose="docker compose -p $project -f $ROOT/compose.quality.yaml"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
$compose down --volumes --remove-orphans >/dev/null 2>&1 || true
|
$compose down --volumes --remove-orphans >/dev/null 2>&1 || true
|
||||||
docker rm --force "$socket_proxy_container" >/dev/null 2>&1 || true
|
docker rm --force "$socket_proxy_container" >/dev/null 2>&1 || true
|
||||||
docker image rm "$quality_image" "$assets_image" "$e2e_image" "$release_image" \
|
docker image rm "$quality_image" "$assets_image" "$e2e_image" "$release_image" \
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,59 @@ fi
|
||||||
: "${POSTGRES_USER:?Set POSTGRES_USER in .env}"
|
: "${POSTGRES_USER:?Set POSTGRES_USER in .env}"
|
||||||
: "${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}"
|
: "${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}"
|
||||||
|
|
||||||
docker compose -p who_need_help up -d --wait db
|
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||||
docker build --target test --tag who-need-help:test .
|
image="who-need-help:test-$run_id"
|
||||||
|
container="who-need-help-test-$run_id"
|
||||||
|
|
||||||
exec docker run --rm \
|
image_tag_is_referenced() {
|
||||||
|
expected_image=$1
|
||||||
|
|
||||||
|
for container_id in $(docker ps -aq --filter "ancestor=$expected_image"); do
|
||||||
|
observed_image=$(docker inspect --format '{{.Config.Image}}' "$container_id")
|
||||||
|
if [ "$observed_image" = "$expected_image" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
status=$?
|
||||||
|
cleanup_status=0
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if docker inspect "$container" >/dev/null 2>&1; then
|
||||||
|
docker rm -f "$container" >/dev/null 2>&1 || cleanup_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker image inspect "$image" >/dev/null 2>&1; then
|
||||||
|
if image_tag_is_referenced "$image"; then
|
||||||
|
echo "Refusing to remove referenced test image: $image" >&2
|
||||||
|
cleanup_status=1
|
||||||
|
else
|
||||||
|
docker image rm "$image" >/dev/null || cleanup_status=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then
|
||||||
|
status=$cleanup_status
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
docker compose -p who_need_help up -d --wait db
|
||||||
|
docker build --target test --tag "$image" .
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--name "$container" \
|
||||||
--network who_need_help_internal \
|
--network who_need_help_internal \
|
||||||
--env MIX_ENV=test \
|
--env MIX_ENV=test \
|
||||||
--env DB_HOST=db \
|
--env DB_HOST=db \
|
||||||
--env "DB_USER=$POSTGRES_USER" \
|
--env "DB_USER=$POSTGRES_USER" \
|
||||||
--env "DB_PASSWORD=$POSTGRES_PASSWORD" \
|
--env "DB_PASSWORD=$POSTGRES_PASSWORD" \
|
||||||
--env TEST_POOL_SIZE="${TEST_POOL_SIZE:-10}" \
|
--env TEST_POOL_SIZE="${TEST_POOL_SIZE:-10}" \
|
||||||
who-need-help:test \
|
"$image" \
|
||||||
mix test
|
mix test
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,20 @@ defmodule WhoNeedHelpWeb.PageControllerTest do
|
||||||
)
|
)
|
||||||
) == 1
|
) == 1
|
||||||
|
|
||||||
|
assert Enum.count(
|
||||||
|
LazyHTML.query(
|
||||||
|
document,
|
||||||
|
"#home-demo-map[data-map-retry-label='Try map again']"
|
||||||
|
)
|
||||||
|
) == 1
|
||||||
|
|
||||||
|
assert Enum.count(
|
||||||
|
LazyHTML.query(
|
||||||
|
document,
|
||||||
|
"[data-map-status-badge][data-interactive-label='Interactive demo'][data-static-label='Static preview'][aria-live='polite']"
|
||||||
|
)
|
||||||
|
) == 1
|
||||||
|
|
||||||
assert Enum.count(
|
assert Enum.count(
|
||||||
LazyHTML.query(document, "head link[rel='apple-touch-icon'][sizes='180x180']")
|
LazyHTML.query(document, "head link[rel='apple-touch-icon'][sizes='180x180']")
|
||||||
) == 1
|
) == 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user