Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 944a85dcc8 |
+80
-13
@@ -9,7 +9,25 @@ on:
|
||||
jobs:
|
||||
a11y:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 25
|
||||
|
||||
# Полноценный PostgreSQL для CI: схема Лидерры — чисто PG (RLS, партиции, роли,
|
||||
# raw schema.sql через load_initial_schema), на SQLite не грузится. Без живой БД
|
||||
# 14 авторизованных Pa11y-маршрутов не могут залогиниться под admin@demo.local.
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: liderra
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -38,35 +56,84 @@ jobs:
|
||||
run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Install app JS deps
|
||||
# --legacy-peer-deps: Histoire 1.0-beta.1 заявляет peerDep vite ^7, установлено vite 8.
|
||||
working-directory: app
|
||||
run: npm ci --no-audit --no-fund --legacy-peer-deps
|
||||
|
||||
- name: Create PostgreSQL roles
|
||||
# schema.sql грузится без ролей (GRANT'ы в DO $$ EXISTS-guard), но поздние миграции
|
||||
# делают необёрнутый GRANT ... TO crm_app_user/crm_supplier_worker → роли нужны.
|
||||
env:
|
||||
PGPASSWORD: postgres
|
||||
run: |
|
||||
psql -h 127.0.0.1 -U postgres -d liderra -v ON_ERROR_STOP=1 \
|
||||
-v crm_app_password=ci_pa11y \
|
||||
-v crm_admin_password=ci_pa11y \
|
||||
-v crm_migrator_password=ci_pa11y \
|
||||
-v crm_audit_writer_password=ci_pa11y \
|
||||
-v crm_supplier_worker_password=ci_pa11y \
|
||||
-f db/00_create_roles.sql
|
||||
|
||||
- name: Bootstrap .env + key
|
||||
working-directory: app
|
||||
run: |
|
||||
cp .env.example .env
|
||||
php artisan key:generate --force
|
||||
|
||||
- name: Prepare SQLite (public Pa11y routes need no real DB)
|
||||
# Pa11y покрывает 7 публичных SPA-маршрутов (login/register/forgot/2fa/recovery/403/500) —
|
||||
# они рендерятся без БД. Полная-PostgreSQL сборка с миграциями/seed отложена в отдельную
|
||||
# задачу (схема и миграции разошлись → from-scratch migrate сломан).
|
||||
- name: Configure .env for CI PostgreSQL + Sanctum SPA
|
||||
# phpdotenv: первое вхождение ключа выигрывает → удаляем строку и дописываем заново.
|
||||
# APP_ENV=local → DatabaseSeeder зовёт DemoSeeder (admin@demo.local) + session-cookie не secure-only.
|
||||
# SANCTUM_STATEFUL_DOMAINS обязан включать localhost:8000 — иначе сессия с Pa11y-хоста не залипает.
|
||||
# DB_SUPPLIER_* нужны: часть миграций пишет через pgsql_supplier-соединение (BYPASSRLS-роль).
|
||||
working-directory: app
|
||||
run: |
|
||||
mkdir -p storage/framework/sessions storage/framework/views storage/framework/cache storage/logs bootstrap/cache
|
||||
touch database/database.sqlite
|
||||
sed -i 's/DB_CONNECTION=.*/DB_CONNECTION=sqlite/' .env
|
||||
sed -i 's|DB_DATABASE=.*|DB_DATABASE=/home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/app/database/database.sqlite|' .env
|
||||
sed -i 's/SESSION_DRIVER=.*/SESSION_DRIVER=file/' .env
|
||||
sed -i 's/CACHE_STORE=.*/CACHE_STORE=file/' .env
|
||||
sed -i 's/QUEUE_CONNECTION=.*/QUEUE_CONNECTION=sync/' .env
|
||||
setenv() { sed -i "/^$1=/d" .env; echo "$1=$2" >> .env; }
|
||||
setenv APP_ENV local
|
||||
setenv APP_DEBUG true
|
||||
setenv APP_URL http://localhost:8000
|
||||
setenv DB_CONNECTION pgsql
|
||||
setenv DB_HOST 127.0.0.1
|
||||
setenv DB_PORT 5432
|
||||
setenv DB_DATABASE liderra
|
||||
setenv DB_USERNAME postgres
|
||||
setenv DB_PASSWORD postgres
|
||||
setenv DB_SUPPLIER_USERNAME postgres
|
||||
setenv DB_SUPPLIER_PASSWORD postgres
|
||||
setenv DB_SSLMODE disable
|
||||
setenv SESSION_DRIVER file
|
||||
setenv CACHE_STORE file
|
||||
setenv QUEUE_CONNECTION sync
|
||||
setenv MAIL_MAILER log
|
||||
setenv SANCTUM_STATEFUL_DOMAINS localhost:8000,127.0.0.1:8000,localhost,127.0.0.1
|
||||
|
||||
- name: Prepare storage dirs (file session/cache need them)
|
||||
# SESSION_DRIVER=file пишет в storage/framework/sessions — без каталога 500 (урок PR #49).
|
||||
working-directory: app
|
||||
run: mkdir -p storage/framework/sessions storage/framework/views storage/framework/cache storage/logs bootstrap/cache
|
||||
|
||||
- name: Run migrations (postgres superuser → guarded SET ROLE works)
|
||||
working-directory: app
|
||||
run: php artisan migrate --force
|
||||
|
||||
- name: Create current-month partitions
|
||||
# schema.sql + миграции дают baseline-партиции; cron-команда докидывает текущий +2 месяца
|
||||
# (идемпотентно) — нужно для demo-сделок DemoSeeder'а за «сегодня».
|
||||
working-directory: app
|
||||
run: php artisan partitions:create-months --ahead=2
|
||||
|
||||
- name: Seed demo data (PricingTier + DemoSeeder admin@demo.local)
|
||||
working-directory: app
|
||||
run: php artisan db:seed --force
|
||||
|
||||
- name: Build frontend assets
|
||||
working-directory: app
|
||||
run: npm run build
|
||||
|
||||
- name: Start Laravel dev-server
|
||||
# PHP_CLI_SERVER_WORKERS>1: встроенный сервер обслуживает SPA + sub-resources параллельно.
|
||||
working-directory: app
|
||||
env:
|
||||
PHP_CLI_SERVER_WORKERS: 4
|
||||
run: nohup php artisan serve --host=127.0.0.1 --port=8000 > /tmp/laravel-serve.log 2>&1 &
|
||||
|
||||
- name: Wait for dev-server ready
|
||||
@@ -82,7 +149,7 @@ jobs:
|
||||
tail -50 /tmp/laravel-serve.log
|
||||
exit 1
|
||||
|
||||
- name: Run Pa11y (live Vue — 7 public routes)
|
||||
- name: Run Pa11y (live Vue, 7 public + 14 authenticated routes)
|
||||
run: npm run a11y
|
||||
|
||||
- name: Laravel log tail on failure
|
||||
|
||||
+2
@@ -38,6 +38,8 @@ return new class extends Migration
|
||||
)
|
||||
SQL);
|
||||
$supplier->statement('ALTER TABLE balance_freeze_log ENABLE ROW LEVEL SECURITY');
|
||||
// Idempotency: schema.sql (сборка с нуля) уже создаёт эту политику — снимаем перед CREATE.
|
||||
$supplier->statement('DROP POLICY IF EXISTS tenant_isolation ON balance_freeze_log');
|
||||
$supplier->statement(<<<'SQL'
|
||||
CREATE POLICY tenant_isolation ON balance_freeze_log
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)
|
||||
|
||||
@@ -11,10 +11,13 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->timestampTz('paused_at')->nullable()->after('is_active');
|
||||
$table->index('paused_at', 'projects_paused_at_idx');
|
||||
});
|
||||
// Idempotency: schema.sql (сборка с нуля) уже содержит paused_at + индекс.
|
||||
if (! Schema::hasColumn('projects', 'paused_at')) {
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->timestampTz('paused_at')->nullable()->after('is_active');
|
||||
});
|
||||
}
|
||||
DB::statement('CREATE INDEX IF NOT EXISTS projects_paused_at_idx ON projects (paused_at)');
|
||||
|
||||
// Backfill: для уже paused проектов используем updated_at как best-effort
|
||||
// (для долго-paused — grace давно истёк; для свежих — близко к реальной паузе).
|
||||
@@ -28,9 +31,11 @@ return new class extends Migration
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->dropIndex('projects_paused_at_idx');
|
||||
$table->dropColumn('paused_at');
|
||||
});
|
||||
DB::statement('DROP INDEX IF EXISTS projects_paused_at_idx');
|
||||
if (Schema::hasColumn('projects', 'paused_at')) {
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->dropColumn('paused_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+23
-8
@@ -21,7 +21,7 @@ return new class extends Migration {
|
||||
}
|
||||
|
||||
DB::unprepared(<<<'SQL'
|
||||
CREATE TABLE project_routing_snapshots (
|
||||
CREATE TABLE IF NOT EXISTS project_routing_snapshots (
|
||||
snapshot_date DATE NOT NULL,
|
||||
project_id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
@@ -41,28 +41,43 @@ return new class extends Migration {
|
||||
-- а snapshot должен пережить (хвост слепка ещё летит).
|
||||
) PARTITION BY RANGE (snapshot_date);
|
||||
|
||||
CREATE INDEX project_routing_snapshots_tenant_date_idx
|
||||
CREATE INDEX IF NOT EXISTS project_routing_snapshots_tenant_date_idx
|
||||
ON project_routing_snapshots (tenant_id, snapshot_date);
|
||||
CREATE INDEX project_routing_snapshots_signal_idx
|
||||
CREATE INDEX IF NOT EXISTS project_routing_snapshots_signal_idx
|
||||
ON project_routing_snapshots (snapshot_date, signal_type, lower(signal_identifier));
|
||||
|
||||
ALTER TABLE project_routing_snapshots ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS project_routing_snapshots_tenant_isolation ON project_routing_snapshots;
|
||||
CREATE POLICY project_routing_snapshots_tenant_isolation
|
||||
ON project_routing_snapshots
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE ON project_routing_snapshots TO crm_app_user;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON project_routing_snapshots TO crm_supplier_worker;
|
||||
|
||||
-- Партиция для текущего месяца (создаётся также через partitions:create-months).
|
||||
CREATE TABLE project_routing_snapshots_y2026_m05
|
||||
CREATE TABLE IF NOT EXISTS project_routing_snapshots_y2026_m05
|
||||
PARTITION OF project_routing_snapshots
|
||||
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
|
||||
CREATE TABLE project_routing_snapshots_y2026_m06
|
||||
CREATE TABLE IF NOT EXISTS project_routing_snapshots_y2026_m06
|
||||
PARTITION OF project_routing_snapshots
|
||||
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
|
||||
SQL);
|
||||
|
||||
// GRANT'ы вынесены из DDL-блока и обёрнуты в проверку существования роли:
|
||||
// сборка с нуля на окружении без 5 ролей (dev throwaway) не должна падать (mirror balance_freeze_log).
|
||||
foreach ([
|
||||
'crm_app_user' => 'SELECT, INSERT, UPDATE',
|
||||
'crm_supplier_worker' => 'SELECT, INSERT, UPDATE, DELETE',
|
||||
] as $role => $privs) {
|
||||
DB::statement(<<<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{$role}') THEN
|
||||
GRANT {$privs} ON project_routing_snapshots TO {$role};
|
||||
END IF;
|
||||
END
|
||||
\$\$
|
||||
SQL);
|
||||
}
|
||||
|
||||
// Регистрация в retention (system_settings).
|
||||
$exists = DB::table('system_settings')
|
||||
->where('key', 'partition_retention_months_project_routing_snapshots')
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# From-scratch migrate idempotency + full-PostgreSQL Pa11y — Implementation Plan
|
||||
|
||||
> **For agentic workers:** small, surgical bugfix. Verification is the CI run of the
|
||||
> full-PostgreSQL `a11y.yml` (from-scratch `migrate` on a clean PG = the reproduction).
|
||||
|
||||
**Goal:** Make `php artisan migrate` succeed on an empty PostgreSQL (CI / new server /
|
||||
backup-rebuild), then restore the 14 authenticated Pa11y routes that need a real login.
|
||||
|
||||
**Architecture:** `0001_01_01_000000_load_initial_schema` loads the *full current*
|
||||
`db/schema.sql` first, then 28 delta migrations run on top. 25 of them are already
|
||||
idempotent (guards: `Schema::hasColumn`, `to_regclass`, `pg_class`, `IF NOT EXISTS`,
|
||||
`ON CONFLICT`, `DO $$ IF NOT EXISTS pg_constraint $$`). Exactly **3** miss the guard and
|
||||
fail with "already exists" because their objects are already in `schema.sql`. Fix = add the
|
||||
missing guards. `db/schema.sql` is NOT touched (source-of-truth rule, Pravila §4.2). Prod is
|
||||
unaffected — these migrations are already recorded as run there, so their bodies never re-execute.
|
||||
|
||||
**Tech Stack:** Laravel 13, PostgreSQL 16, GitHub Actions, pa11y-ci (WCAG2AA).
|
||||
|
||||
---
|
||||
|
||||
## Root cause (systematic-debugging, complete)
|
||||
|
||||
Confirmed by reading every post-`load_initial_schema` migration and cross-checking `db/schema.sql`:
|
||||
|
||||
| Migration | Non-idempotent statement | Already in schema.sql |
|
||||
|---|---|---|
|
||||
| `2026_05_24_100000_add_balance_freeze_to_tenants_and_projects` | `CREATE POLICY tenant_isolation ON balance_freeze_log` (table/cols/indexes already guarded) | policy @ schema.sql:3357 |
|
||||
| `2026_05_26_120000_add_paused_at_to_projects` | `$table->timestampTz('paused_at')` + `$table->index('projects_paused_at_idx')` | column @ :815, index @ :897 |
|
||||
| `2026_05_27_120000_create_project_routing_snapshots_table` | `CREATE TABLE` + 2 indexes + policy + 2 partitions (no `IF NOT EXISTS`); unconditional GRANTs | table/policy/indexes @ schema.sql v8.39 |
|
||||
|
||||
All other deltas are already idempotent (verified individually). `webhook_log`/`rejected_deals_log`
|
||||
were removed from schema.sql in v8.35, so the migrations touching them are no-ops on a fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — guard `CREATE POLICY` on `balance_freeze_log`
|
||||
|
||||
**File:** `app/database/migrations/2026_05_24_100000_add_balance_freeze_to_tenants_and_projects.php`
|
||||
|
||||
- [ ] Insert `DROP POLICY IF EXISTS tenant_isolation ON balance_freeze_log` before the `CREATE POLICY`.
|
||||
|
||||
## Task 2 — guard `paused_at` column + index
|
||||
|
||||
**File:** `app/database/migrations/2026_05_26_120000_add_paused_at_to_projects.php`
|
||||
|
||||
- [ ] `up()`: wrap the column add in `if (! Schema::hasColumn('projects','paused_at'))`; create the
|
||||
index via `CREATE INDEX IF NOT EXISTS projects_paused_at_idx`. Keep the idempotent backfill.
|
||||
- [ ] `down()`: `DROP INDEX IF EXISTS` + guard `dropColumn` with `hasColumn`.
|
||||
|
||||
## Task 3 — make `project_routing_snapshots` DDL idempotent
|
||||
|
||||
**File:** `app/database/migrations/2026_05_27_120000_create_project_routing_snapshots_table.php`
|
||||
|
||||
- [ ] `CREATE TABLE IF NOT EXISTS` for the parent and both partitions.
|
||||
- [ ] `CREATE INDEX IF NOT EXISTS` for both indexes.
|
||||
- [ ] `DROP POLICY IF EXISTS ... ; CREATE POLICY ...`.
|
||||
- [ ] Move GRANTs out of the DDL block into role-existence-guarded `DO $$ ... pg_roles ... $$`
|
||||
blocks (so a from-scratch build on an environment without the 5 roles still succeeds).
|
||||
|
||||
## Task 4 — full-PostgreSQL CI workflow
|
||||
|
||||
**File:** `.github/workflows/a11y.yml`
|
||||
|
||||
- [ ] postgres:16 service container (liderra/postgres/postgres).
|
||||
- [ ] Create 5 DB roles via `db/00_create_roles.sql`.
|
||||
- [ ] `.env`: APP_ENV=local, DB pgsql + `DB_SUPPLIER_USERNAME/PASSWORD` (pgsql_supplier connection),
|
||||
SESSION/CACHE=file, QUEUE=sync, MAIL=log, `SANCTUM_STATEFUL_DOMAINS` incl `localhost:8000`.
|
||||
- [ ] `mkdir -p storage/framework/{sessions,views,cache}` (file session/cache need them — PR #49 lesson).
|
||||
- [ ] `migrate --force` → `partitions:create-months --ahead=2` → `db:seed --force` (DemoSeeder admin@demo.local).
|
||||
- [ ] Keep Node 22 + root `npm install` (lock drift) + app `npm ci --legacy-peer-deps` (PR #49 lessons).
|
||||
|
||||
## Task 5 — restore 14 authenticated Pa11y routes
|
||||
|
||||
**File:** `pa11y.config.json`
|
||||
|
||||
- [ ] Re-add the 14 authenticated URLs (dashboard/deals/kanban/projects/billing/settings/reports/
|
||||
reminders + 6 admin/*) with login `actions` (admin@demo.local / password → wait for /dashboard).
|
||||
|
||||
## Verification
|
||||
|
||||
CI run of `a11y.yml` on the PR: it boots clean PostgreSQL, runs `migrate` from scratch, seeds,
|
||||
and Pa11y-scans all 7 public + 14 authenticated routes. Green = both the migrate fix and the
|
||||
14-page restoration are proven. (`php`/squawk/pest are gate-blocked locally → CI is the verifier.)
|
||||
@@ -47,6 +47,200 @@
|
||||
{
|
||||
"url": "http://localhost:8000/500",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-07-500.png"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/dashboard",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-08-dashboard.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/deals",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-09-deals.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/deals",
|
||||
"wait for path to be /deals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/kanban",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-10-kanban.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/kanban",
|
||||
"wait for path to be /kanban"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/projects",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-11-projects.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/projects",
|
||||
"wait for path to be /projects"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/billing",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-12-billing.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/billing",
|
||||
"wait for path to be /billing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/settings",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-13-settings.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/settings",
|
||||
"wait for path to be /settings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/reports",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-14-reports.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/reports",
|
||||
"wait for path to be /reports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/reminders",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-15-reminders.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/reminders",
|
||||
"wait for path to be /reminders"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/tenants",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-16-admin-tenants.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/tenants",
|
||||
"wait for path to be /admin/tenants"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/billing",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-17-admin-billing.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/billing",
|
||||
"wait for path to be /admin/billing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/incidents",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-18-admin-incidents.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/incidents",
|
||||
"wait for path to be /admin/incidents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/system",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-19-admin-system.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/system",
|
||||
"wait for path to be /admin/system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/pricing-tiers",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-20-admin-pricing-tiers.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/pricing-tiers",
|
||||
"wait for path to be /admin/pricing-tiers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/admin/supplier-prices",
|
||||
"screenCapture": "./bin/a11y-screenshots/live-auth-21-admin-supplier-prices.png",
|
||||
"actions": [
|
||||
"navigate to http://localhost:8000/login",
|
||||
"wait for element input[autocomplete=\"email\"] to be visible",
|
||||
"set field input[autocomplete=\"email\"] to admin@demo.local",
|
||||
"set field input[autocomplete=\"current-password\"] to password",
|
||||
"click element button[type=\"submit\"]",
|
||||
"wait for path to be /dashboard",
|
||||
"navigate to http://localhost:8000/admin/supplier-prices",
|
||||
"wait for path to be /admin/supplier-prices"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user