# Q.DEFER.004 — A11y residual patterns Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Закрыть три a11y-паттерна, оставшиеся после Q.DEFER.002 sub-B batch-фикса: (A) DealsTable VDataTable `show-select` 6 bulk-checkbox без aria-label, (B) AdminSupplierPricesView 9 form-input без aria-label, (C) v-tooltip eager-mount artifact (документировать, не код).

**Architecture:** Используем Vuetify 3.12 typed slots `header.data-table-select` + `item.data-table-select` (DealsTable) и `:aria-label` binding на v-text-field/v-switch (AdminSupplierPricesView). Vitest specs проверяют наличие aria-label через DOM query; Playwright + axe-core CDN inject верифицирует **0 violations** на live-страницах. Для (C) — документирующее изменение в `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md` и `findings.md`.

**Tech Stack:** Vue 3.5 + Vuetify 3.12 + TypeScript + Vitest 4.1 + @vue/test-utils + Playwright MCP + axe-core 4.10 (CDN inject).

---

## File Structure

**Modify (production code):**

- `app/resources/js/components/deals/DealsTable.vue` — добавить два slot-override для bulk-checkboxes с aria-label.
- `app/resources/js/views/admin/AdminSupplierPricesView.vue` — добавить `:aria-label` на 3× v-text-field + 3× v-switch + 3× cost_rub v-text-field (по факту это 3 input × 3 supplier rows = 9 элементов).

**Modify (test code):**

- `app/tests/Frontend/AdminSupplierPricesView.spec.ts` — добавить spec на проверку aria-label на 9 элементах.
- `app/tests/Frontend/DealsTable.spec.ts` — **новый файл**, проверка bulk-checkbox aria-label через `show-select` slot override.

**Modify (docs):**

- `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md` — Q.DEFER.004 пометить closed (A + B fixed, C documented as known Vuetify-internal limitation).
- `docs/superpowers/audits/2026-05-12-portal-full-audit-findings.md` — закрыть finding в Phase rollup.

**Memory:**

- `memory/project_full_audit_2026-05-12.md` — обновить description + content (Q.DEFER.004 closure status).
- `memory/MEMORY.md` — sync description строки.

---

## Task 1: DealsTable show-select aria-label slots

**Files:**

- Modify: `app/resources/js/components/deals/DealsTable.vue:54-74` (vue template — VDataTable + новые slots между `@click:row` и закрывающим `</v-data-table>`).
- Create: `app/tests/Frontend/DealsTable.spec.ts`.

- [ ] **Step 1: Write failing test** (Vitest spec, mount DealsTable, проверить bulk-checkbox aria-label).

Создать `app/tests/Frontend/DealsTable.spec.ts`:

```typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';

import DealsTable from '../../resources/js/components/deals/DealsTable.vue';
import type { MockDeal } from '../../resources/js/composables/mockDeals';

const vuetify = createVuetify();

const sampleDeals: MockDeal[] = [
    {
        id: 1,
        name: 'Иванов И.',
        phone: '+7 (916) 100-00-01',
        statusSlug: 'new',
        project: 'B1 site',
        manager: { initials: 'AD', name: 'Admin' },
        cost: 1000,
        receivedMinutesAgo: 5,
    },
    {
        id: 2,
        name: 'Петров П.',
        phone: '+7 (916) 100-00-02',
        statusSlug: 'new',
        project: 'B1 call',
        manager: { initials: 'AD', name: 'Admin' },
        cost: 1500,
        receivedMinutesAgo: 30,
    },
];

describe('DealsTable a11y (Q.DEFER.004 sub-A)', () => {
    it('select-all header checkbox has aria-label', () => {
        const wrapper = mount(DealsTable, {
            props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
            global: { plugins: [vuetify] },
        });
        const headerCheckbox = wrapper.find(
            'th .v-selection-control input[type="checkbox"][aria-label="Выбрать все сделки"]',
        );
        expect(headerCheckbox.exists()).toBe(true);
    });

    it('each row checkbox has aria-label referencing deal name', () => {
        const wrapper = mount(DealsTable, {
            props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
            global: { plugins: [vuetify] },
        });
        const rowCheckbox1 = wrapper.find(
            'tbody tr:nth-of-type(1) input[type="checkbox"][aria-label="Выбрать сделку «Иванов И.»"]',
        );
        const rowCheckbox2 = wrapper.find(
            'tbody tr:nth-of-type(2) input[type="checkbox"][aria-label="Выбрать сделку «Петров П.»"]',
        );
        expect(rowCheckbox1.exists()).toBe(true);
        expect(rowCheckbox2.exists()).toBe(true);
    });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && npx vitest run tests/Frontend/DealsTable.spec.ts --reporter=verbose`
Expected: 2 FAIL — selectors not found (default Vuetify-rendered checkboxes без aria-label).

- [ ] **Step 3: Add VDataTable slots override в DealsTable.vue**

Edit `app/resources/js/components/deals/DealsTable.vue` — между line 124 (`</template>` для `item.receivedMinutesAgo`) и line 125 (`</v-data-table>`):

```vue
            <template #header.data-table-select="{ allSelected, selectAll, someSelected }">
                <v-checkbox-btn
                    :model-value="allSelected"
                    :indeterminate="someSelected && !allSelected"
                    aria-label="Выбрать все сделки"
                    @update:model-value="selectAll"
                />
            </template>

            <template
                #item.data-table-select="{ isSelected, toggleSelect, internalItem, item }: { isSelected: (i: unknown) => boolean; toggleSelect: (i: unknown) => void; internalItem: unknown; item: MockDeal }"
            >
                <v-checkbox-btn
                    :model-value="isSelected(internalItem)"
                    :aria-label="`Выбрать сделку «${item.name}»`"
                    @update:model-value="toggleSelect(internalItem)"
                />
            </template>
```

- [ ] **Step 4: Run vue-tsc to verify slot signatures type-check**

Run: `cd app && npx vue-tsc --noEmit -p tsconfig.json 2>&1 | head -30`
Expected: 0 errors related to DealsTable.vue (baseline 0).

- [ ] **Step 5: Run Vitest to verify a11y tests pass**

Run: `cd app && npx vitest run tests/Frontend/DealsTable.spec.ts --reporter=verbose`
Expected: 2 PASS.

- [ ] **Step 6: Run full Vitest to verify no regression**

Run: `cd app && npx vitest run --reporter=default 2>&1 | tail -30`
Expected: 85 files / 659 passed (+2) / 3 skipped / 0 failed.

- [ ] **Step 7: Commit**

```bash
git add app/resources/js/components/deals/DealsTable.vue app/tests/Frontend/DealsTable.spec.ts
git commit -m "fix(a11y): Q.DEFER.004 sub-A — DealsTable show-select bulk-checkbox aria-label

VDataTable show-select prop генерировал unlabeled checkbox per row + select-all
header — axe-core критичная label violation (6 nodes на demo seed).

Override через Vuetify 3.12 typed slots:
- header.data-table-select → aria-label='Выбрать все сделки'
- item.data-table-select → aria-label='Выбрать сделку «{{name}}»' (per row)

Test coverage: tests/Frontend/DealsTable.spec.ts (2 specs)."
```

---

## Task 2: AdminSupplierPricesView form inputs aria-label

**Files:**

- Modify: `app/resources/js/views/admin/AdminSupplierPricesView.vue:6-31` (3 templates с v-text-field/v-switch).
- Modify: `app/tests/Frontend/AdminSupplierPricesView.spec.ts` (добавить 4-й spec).

- [ ] **Step 1: Write failing test** (расширить существующий spec).

Edit `app/tests/Frontend/AdminSupplierPricesView.spec.ts` — добавить после строки 60:

```typescript
    it('each input/switch has explicit aria-label combining supplier name + field role', async () => {
        const wrapper = mount(AdminSupplierPricesView, { global: { plugins: [vuetify] } });
        await new Promise((r) => setTimeout(r, 50));
        // 3 suppliers × 3 fields = 9 controls
        const expectedLabels = [
            'Cost (₽) для B1 — Сайты и Звонки',
            'Quality для B1 — Сайты и Звонки',
            'Active для B1 — Сайты и Звонки',
            'Cost (₽) для B2 — SMS',
            'Quality для B2 — SMS',
            'Active для B2 — SMS',
            'Cost (₽) для B3 — SMS',
            'Quality для B3 — SMS',
            'Active для B3 — SMS',
        ];
        for (const label of expectedLabels) {
            const node = wrapper.find(`[aria-label="${label}"]`);
            expect(node.exists(), `aria-label="${label}" not found`).toBe(true);
        }
    });
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && npx vitest run tests/Frontend/AdminSupplierPricesView.spec.ts --reporter=verbose`
Expected: PASS×3 (existing) + FAIL×1 (new — aria-label not present).

- [ ] **Step 3: Apply :aria-label на 3 templates в AdminSupplierPricesView.vue**

Edit `app/resources/js/views/admin/AdminSupplierPricesView.vue` — `<template #[`item.cost_rub`]>`:

```vue
                <template #[`item.cost_rub`]="{ item }">
                    <v-text-field
                        v-model="item.cost_rub"
                        type="number"
                        step="0.01"
                        min="0"
                        density="compact"
                        hide-details
                        variant="plain"
                        :aria-label="`Cost (₽) для ${item.name}`"
                    />
                </template>
```

Same pattern для `item.quality_score`:

```vue
                <template #[`item.quality_score`]="{ item }">
                    <v-text-field
                        v-model="item.quality_score"
                        type="number"
                        step="0.01"
                        min="0"
                        max="9.99"
                        density="compact"
                        hide-details
                        variant="plain"
                        :aria-label="`Quality для ${item.name}`"
                    />
                </template>
```

И `item.is_active`:

```vue
                <template #[`item.is_active`]="{ item }">
                    <v-switch
                        v-model="item.is_active"
                        hide-details
                        inset
                        density="compact"
                        :aria-label="`Active для ${item.name}`"
                    />
                </template>
```

- [ ] **Step 4: Run Vitest to verify a11y test passes**

Run: `cd app && npx vitest run tests/Frontend/AdminSupplierPricesView.spec.ts --reporter=verbose`
Expected: 4 PASS, 0 FAIL.

- [ ] **Step 5: Run full Vitest to verify no regression**

Run: `cd app && npx vitest run --reporter=default 2>&1 | tail -20`
Expected: 85 files / 660 passed (+1) / 3 skipped / 0 failed.

- [ ] **Step 6: Commit**

```bash
git add app/resources/js/views/admin/AdminSupplierPricesView.vue app/tests/Frontend/AdminSupplierPricesView.spec.ts
git commit -m "fix(a11y): Q.DEFER.004 sub-B — AdminSupplierPricesView 9 inputs aria-label

3 supplier rows × 3 form controls (cost_rub v-text-field +
quality_score v-text-field + is_active v-switch) = 9 nodes без label —
axe-core критичная label violation.

Fix: :aria-label='\${field} для \${supplier.name}' (e.g. 'Cost (₽) для B1 — Сайты и Звонки').

Test coverage: AdminSupplierPricesView.spec.ts 4-й spec проверяет все 9 ожидаемых
aria-label через DOM query."
```

---

## Task 3: v-tooltip artifact — документирующее изменение

**Files:**

- Modify: `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md:179-198` (Q.DEFER.004 заголовок + sub-C documentation).
- Modify: `docs/superpowers/audits/2026-05-12-portal-full-audit-findings.md` (closure note в Phase rollup, если есть).

- [ ] **Step 1: Verify через Playwright MCP + axe-core, что v-tooltip artifact реально only-library-level**

Сценарий: запустить dev server (если не запущен), перейти на /admin/tenants авторизованным, заинжектить axe-core CDN, запустить scan, проверить что aria-tooltip-name violation указывает только на Vuetify-internal `<div role="tooltip">` без пользовательского контента и что у соответствующего button уже есть aria-label.

Если запуск Playwright недоступен (servers off) — пропустить шаг, опираться на findings из Q.DEFER.002 sub-B (там уже задокументировано: "button already has aria-label → effective a11y покрытие OK").

- [ ] **Step 2: Обновить blocked.md Q.DEFER.004 status**

Edit `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md` — заменить заголовок секции `### Q.DEFER.004` блоком:

```markdown
### Q.DEFER.004 — ✅ CLOSED 2026-05-13 — A+B fixed via slots/aria-label, C documented as Vuetify-internal known limitation

**Closure summary (13.05.2026 day session):** sub-A + sub-B fixed (commits `<TBD-A>` + `<TBD-B>`). sub-C — Vuetify v-tooltip eager-mount artifact задокументирован как **known library-level limitation**, не application defect (button уже имеет explicit `aria-label`, effective a11y покрытие OK; axe-core complains только о Vuetify-internal `<div role="tooltip">` pre-emptively созданном без content).

**Sub-A details:** DealsTable.vue — Vuetify 3.12 typed slots `header.data-table-select` + `item.data-table-select` override; 6 bulk-checkboxes теперь имеют aria-label «Выбрать все сделки» + «Выбрать сделку «<name>»». Test coverage: tests/Frontend/DealsTable.spec.ts (2 specs).

**Sub-B details:** AdminSupplierPricesView.vue — `:aria-label` binding на 3× v-text-field (cost_rub) + 3× v-text-field (quality_score) + 3× v-switch (is_active) = 9 elements. Pattern: «<Field> для <supplier.name>» (e.g. «Cost (₽) для B1 — Сайты и Звонки»). Test coverage: AdminSupplierPricesView.spec.ts 4-й spec.

**Sub-C status:** Vuetify v-tooltip создаёт `<div role="tooltip" id="v-tooltip-v-N">` pre-emptively (до показа), без content. axe-core reports aria-tooltip-name serious violation. **Application-level mitigation уже на месте:** activator button (TenantsTable.vue:89) имеет `:aria-label="Войти как клиент (impersonation) для ${item.name}"`. Workaround «убрать v-tooltip и использовать HTML title» — теряем Vuetify styling + теряем tooltip activator behavior; не оправдано ради cosmetic violation. **Upstream fix tracking:** оставить как known issue до Vuetify 3.13+ (если содержит fix), либо до polish-sprint post-MVP.

---

(оригинальное содержание Q.DEFER.004 ниже — оставить как historical context)

**Из:** Q.DEFER.002 sub-B audit-pass 12.05.2026 ночь.
...
```

(полностью заменить только заголовок + closure summary, оставить historical context A/B/C body как был).

- [ ] **Step 3: Обновить findings.md (если у Q.DEFER.004 есть отдельная строка)**

Run: `grep -n "Q.DEFER.004" docs/superpowers/audits/2026-05-12-portal-full-audit-findings.md`

Если найдено — добавить статус-метку «✅ CLOSED 2026-05-13» рядом с упоминанием. Если нет — пропустить.

- [ ] **Step 4: Commit (документация)**

```bash
git add docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md docs/superpowers/audits/2026-05-12-portal-full-audit-findings.md
git commit -m "docs(audit): Q.DEFER.004 CLOSED — sub-A+B fixed, sub-C documented as Vuetify-internal known limitation"
```

---

## Task 4: Re-axe verify через Playwright + axe-core CDN inject

**Files:** read-only verification, не git-tracked. Артефакты — scrennshots в `audit-screens/q-defer-004/` опционально.

- [ ] **Step 1: Запустить dev server, авторизоваться, заинжектить axe-core**

Servers: Laravel `http://127.0.0.1:8000` + Vite Vite-port (5173+). Если уже работают — skip startup.

Через Playwright MCP:

1. `browser_navigate` → `http://127.0.0.1:8000/login`
2. `browser_fill_form` → email=`admin@demo.local`, password=`password`
3. `browser_click` → submit
4. `browser_navigate` → `http://127.0.0.1:8000/deals`
5. `browser_evaluate`:

   ```javascript
   await (async () => {
       const s = document.createElement('script');
       s.src = 'https://cdn.jsdelivr.net/npm/axe-core@4.10.0/axe.min.js';
       document.head.appendChild(s);
       await new Promise((r) => (s.onload = r));
       const r = await window.axe.run();
       return r.violations.filter((v) => v.id === 'label').map((v) => ({ nodes: v.nodes.length, target: v.nodes[0]?.target }));
   })();
   ```

Expected: `label` violations on `/deals` related to data-table-select: **0** (после Task 1).

- [ ] **Step 2: Same scan на /admin/supplier-prices**

Через Playwright MCP:

1. `browser_navigate` → `http://127.0.0.1:8000/admin/supplier-prices`
2. `browser_evaluate` (тот же axe.run() snippet).

Expected: `label` violations: **0** (после Task 2).

- [ ] **Step 3: Same scan на /admin/tenants для подтверждения v-tooltip status**

Через Playwright MCP:

1. `browser_navigate` → `http://127.0.0.1:8000/admin/tenants`
2. `browser_evaluate` тот же snippet с фильтром `r.violations.filter((v) => v.id === 'aria-tooltip-name')`.

Expected: 1 violation **с подтверждением что target — Vuetify-internal `div[role="tooltip"]`**, button has aria-label (verifiable из same DOM). Это документирует sub-C — НЕ application defect.

- [ ] **Step 4: Записать результаты в blocked.md sub-C verification log**

Edit `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md` Q.DEFER.004 — добавить блок «Verification 2026-05-13»:

```markdown
**Verification 2026-05-13:** axe-core 4.10 CDN inject на 3 страницах:
- `/deals` (post sub-A fix): 0 `label` violations on data-table-select ✅
- `/admin/supplier-prices` (post sub-B fix): 0 `label` violations on form inputs ✅
- `/admin/tenants` (sub-C): 1 `aria-tooltip-name` violation на Vuetify-internal `div[role="tooltip"]`, button[aria-label] активатор a11y-OK — documented limitation.
```

- [ ] **Step 5: Commit verification log**

```bash
git add docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md
git commit -m "docs(audit): Q.DEFER.004 — Playwright+axe-core verification 2026-05-13"
```

---

## Task 5: Memory + final verification

**Files:**

- Modify: `memory/project_full_audit_2026-05-12.md` (description + content).
- Modify: `memory/MEMORY.md` (sync description line).

- [ ] **Step 1: Update memory project_full_audit_2026-05-12.md**

Add closure line for Q.DEFER.004 в content (преамбула / Q-items list):

```
- Q.DEFER.004 ✅ CLOSED 2026-05-13 — sub-A (DealsTable show-select slots) + sub-B (AdminSupplierPricesView 9 inputs aria-label) fixed via Vuetify 3.12 slots + :aria-label binding. sub-C (v-tooltip eager-mount) documented as Vuetify-internal known limitation, button activator has aria-label.
```

Update frontmatter `description` если есть «Q.DEFER.004 new» — заменить на «Q.DEFER.004 closed».

- [ ] **Step 2: Update MEMORY.md description line for that memory**

Run: `grep -n "project_full_audit_2026-05-12" memory/MEMORY.md`
Edit ту строку — заменить «Q.DEFER.004 new» на «Q.DEFER.004 closed».

- [ ] **Step 3: Run full pre-commit suite for сanity**

```bash
cd app && npx vitest run 2>&1 | tail -10
```

Expected: 85 files / 660 passed / 3 skipped / 0 failed.

```bash
cd app && composer test 2>&1 | tail -10
```

Expected: Pest baseline preserved (742 / 736 sequential / 739 parallel / 3 skipped, no new failures).

- [ ] **Step 4: Pre-push hooks (lefthook)**

```bash
lefthook run pre-push 2>&1 | tail -40
```

Expected: gitleaks-full-history OK + lychee OK + 0 leaks.

- [ ] **Step 5: Push to origin/plan5-frontend-projects**

```bash
git push origin plan5-frontend-projects
```

- [ ] **Step 6: Final commit summary**

Verify branch state:

```bash
git log --oneline -10 plan5-frontend-projects
git rev-list --count origin/main..plan5-frontend-projects
```

Expected: 4+ new commits ahead beyond `a5e99ba`, branch tip = последний commit от Task 5.

---

## Verification Checklist (Self-Review)

После всех 5 Task — ручной checklist:

- [ ] **Spec coverage:** A (DealsTable 6 nodes) + B (9 inputs) + C (v-tooltip documented) — все три закрыты?
- [ ] **No regression:** Pest 739/736 parallel/sequential preserved; Vitest baseline + 3 new specs; lychee 0 broken; gitleaks 0; markdownlint untracked-design.md only.
- [ ] **Verified via live a11y scan:** Playwright + axe-core 4.10 на /deals + /admin/supplier-prices + /admin/tenants подтверждают 0 `label` violations на (A) + (B), 1 documented `aria-tooltip-name` на (C).
- [ ] **Memory + Q-registry sync:** blocked.md Q.DEFER.004 ✅ CLOSED, memory project_full_audit_2026-05-12 updated.
- [ ] **All commits pushed:** `git push origin plan5-frontend-projects` succeed; new tip recorded.

---

## Известные риски

1. **DealsTable.spec.ts mount без store:** компонент не имеет direct store dependency (только pure props), Pinia mount не требуется. Если test после первого запуска fails с «store undefined» — изучить Vuetify 3.12 selection state internals и при необходимости добавить `vuetify` plugin с явным config. **Не блокер.**
2. **Vuetify 3.12 slot signature drift:** если `internalItem` API изменился между minor versions — vue-tsc Step 4 поймает; fallback — использовать только `item: MockDeal` + `isSelected: (item) => boolean` без `internalItem`. **Не блокер.**
3. **Playwright servers (Task 4):** если 8000+5175 уже work — skip startup; если нет — запуск отдельной командой `php artisan serve` + `npm run dev` (background), затем продолжать. **Не блокер.**
4. **C — Vuetify upstream:** если в Vuetify 3.13+ появится fix v-tooltip eager-mount — закрыть sub-C через `composer update vuetify` (не в скоупе этого плана).

---

## Estimated effort

- Task 1 (DealsTable slots): 25 мин
- Task 2 (AdminSupplierPrices aria-label): 20 мин
- Task 3 (v-tooltip documentation): 10 мин
- Task 4 (Playwright + axe verify): 15 мин
- Task 5 (memory + push): 10 мин

**Total: ~80 минут.**

---

## Addendum 2026-05-13 — Q.DEFER.004 part 2 (Vuetify aria deep workaround)

**Trigger:** Task 4 (commit `e79fe95`) выявил, что Tasks 1+2 source-fixes (`:aria-label` prop на `<v-checkbox-btn>` / `<v-text-field>` / `<v-switch>`) **не пробивают Vuetify 3.12 render layer** — native `<input>` остаётся без accessible name. Vitest tests 4/4 проходят в jsdom (видимо, jsdom не симулирует Vuetify VSelectionControl attrs split), но axe-core 4.10 в Chromium показывает 6+9=15 critical `label` violations.

**Decision (user 13.05.2026):** «Investigate Vuetify deep workarounds» — попробовать рабочий pattern.

**Hypothesized fix (highest probability):** Vuetify `<v-checkbox-btn>` / `<v-text-field>` / `<v-switch>` имеют prop `label` который генерирует реальный `<label for="${input.id}">${label}</label>` element. axe-core корректно ассоциирует через `<label for>` → input accessible name. CSS visually-hidden class (clip-path/position absolute) скрывает текстуально, но preserve в accessibility tree. Это стандартный screen-reader-only pattern.

**Alternative patterns if hypothesis fails:**

- Native `<input>` в slot override (bypass Vuetify rendering)
- Explicit `<label class="sr-only" :for="id">` wrap (требует binding на auto-generated id)
- aria-labelledby reference на existing row name cell (Cleanest semantically, но требует ID-binding)

---

### Task 6: Investigation — Vuetify VSelectionControl aria forwarding

**Files:** read-only. Output: prototype/recommendation summary inline в этом плане ниже Task 6.

- [ ] **Step 1: Read Vuetify 3.12 v-checkbox-btn / v-text-field / v-switch implementation**

Через `mcp__plugin_context7_context7__query-docs` либо browsing Vuetify GitHub source `vuetifyjs/vuetify@v3.12.x`. Цель: verify что `label` prop производит `<label for="...">` element в shadow DOM. Document slot-architecture (VSelectionControl base for checkbox/switch).

- [ ] **Step 2: Prototype в minimal Vitest spec**

Создать temporary `app/tests/Frontend/_a11yProbe.spec.ts` (delete after Task 6 closes). Mount 3 variants:

- A: `<v-checkbox-btn label="Test" />` — verify `<label for="id">Test</label>` exists.
- B: `<v-checkbox-btn aria-label="Test" />` (existing approach) — verify it does/doesn't reach input.
- C: native `<input type="checkbox" aria-label="Test" />` (control).

Run via `cd app && npx vitest run tests/Frontend/_a11yProbe.spec.ts`.

- [ ] **Step 3: Live Playwright + axe prototype**

Servers уже работают. Через Playwright MCP открыть простую страницу с прототипом (можно использовать существующий /deals или /admin/supplier-prices с быстрым inline-change). Inject axe-core, проверить `label` violation статус для каждого варианта.

- [ ] **Step 4: Document findings + recommended pattern в этом файле**

Замени этот блок (lines starting от «#### Task 6 — Findings:») на actual results table + recommended pattern (`label` prop + sr-only CSS class).

#### Task 6 — Findings: (to be filled by subagent)

`<заполнить subagent'ом после Step 4>`

---

### Task 7: Re-fix DealsTable bulk-checkbox accessible name

**Files:**

- Modify: `app/resources/js/components/deals/DealsTable.vue:127-141` (2 slot overrides).
- Modify: `app/tests/Frontend/DealsTable.spec.ts:38-58` (test selectors — query `<label>` association, not `[aria-label]` attribute).

- [ ] **Step 1: Apply working pattern в slots**

Based на Task 6 рекомендацию, скорее всего:

```vue
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
    <v-checkbox-btn
        :model-value="allSelected"
        :indeterminate="someSelected && !allSelected"
        label="Выбрать все сделки"
        class="sr-only-label"
        @update:model-value="selectAll"
    />
</template>

<template #item.data-table-select="{ isSelected, toggleSelect, internalItem, item }">
    <v-checkbox-btn
        :model-value="isSelected(internalItem)"
        :label="`Выбрать сделку «${(item as MockDeal).name}»`"
        class="sr-only-label"
        @update:model-value="() => toggleSelect(internalItem)"
    />
</template>
```

И в `<style scoped>`:

```css
.sr-only-label :deep(.v-label) {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
```

- [ ] **Step 2: Update Vitest spec selectors на real-DOM accessibility check**

Заменить `wrapper.find('input[type="checkbox"][aria-label="..."]')` на query `<label>` association:

```typescript
it('select-all header checkbox has accessible name via <label> association', () => {
    const wrapper = mount(...);
    const labels = wrapper.findAll('label');
    const headerLabel = labels.find((l) => l.text() === 'Выбрать все сделки');
    expect(headerLabel?.exists()).toBe(true);
    const forAttr = headerLabel?.attributes('for');
    expect(forAttr).toBeTruthy();
    const input = wrapper.find(`#${forAttr}`);
    expect(input.exists()).toBe(true);
    expect(input.element.tagName).toBe('INPUT');
});
```

(аналогично для row checkboxes).

- [ ] **Step 3: Run Vitest на DealsTable.spec.ts**

Expected 2 PASS (после update).

- [ ] **Step 4: Run full Vitest для no-regression check**

Expected: 85 / 660 / 3 / 0.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/components/deals/DealsTable.vue app/tests/Frontend/DealsTable.spec.ts
git commit -m "fix(a11y): Q.DEFER.004 sub-A part 2 — DealsTable use Vuetify label prop + sr-only CSS

Task 4 verification revealed aria-label НЕ propagates to native <input> через
v-checkbox-btn (Vuetify silently drops). Switch к Vuetify-native pattern:
:label prop generates <label for=id> element + custom CSS visually-hides текст.

axe-core accessible-name теперь видит association через <label for>.

Tests updated к real-DOM accessibility check (label association, not aria-label attr)."
```

---

### Task 8: Re-fix AdminSupplierPricesView accessible names

**Files:**

- Modify: `app/resources/js/views/admin/AdminSupplierPricesView.vue` (3 templates — replace `:aria-label` с `:label` prop + add CSS class).
- Modify: `app/tests/Frontend/AdminSupplierPricesView.spec.ts:62-81` (4-й spec — same DOM check update).

- [ ] **Step 1: Apply working pattern в 3 templates**

```vue
<template #[`item.cost_rub`]="{ item }">
    <v-text-field
        v-model="item.cost_rub"
        type="number" step="0.01" min="0"
        density="compact" hide-details variant="plain"
        :label="`Cost (₽) для ${item.name}`"
        class="sr-only-label"
    />
</template>
```

И аналогично для `quality_score` (label="Quality для ${name}") + `is_active` (v-switch label="Active для ${name}").

CSS блок (`<style scoped>`):

```css
.sr-only-label :deep(.v-label),
.sr-only-label :deep(.v-field-label) {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
```

(Двойной селектор — `.v-label` для v-switch + `.v-field-label` для v-text-field; точные имена verify в Task 6 prototype.)

- [ ] **Step 2: Update Vitest spec на real-DOM accessibility check**

Аналогично Task 7 Step 2 — query `<label>` element с text contents + verify `for` attribute matches input ID.

```typescript
it('each input/switch has accessible name via <label> association', async () => {
    const wrapper = mount(AdminSupplierPricesView, { global: { plugins: [vuetify] } });
    await new Promise((r) => setTimeout(r, 50));
    const expectedTexts = [
        'Cost (₽) для B1 — Сайты и Звонки',
        // ... 9 total
    ];
    for (const text of expectedTexts) {
        const labels = wrapper.findAll('label');
        const found = labels.find((l) => l.text() === text);
        expect(found?.exists(), `label with text "${text}" not found`).toBe(true);
        const forAttr = found?.attributes('for');
        expect(forAttr).toBeTruthy();
        const input = wrapper.find(`#${forAttr}`);
        expect(input.exists(), `input #${forAttr} for "${text}" not found`).toBe(true);
    }
});
```

- [ ] **Step 3: Run target spec**

Expected: 4 PASS.

- [ ] **Step 4: Full Vitest**

Expected: 85 / 660 / 3 / 0.

- [ ] **Step 5: Commit**

```bash
git add app/resources/js/views/admin/AdminSupplierPricesView.vue app/tests/Frontend/AdminSupplierPricesView.spec.ts
git commit -m "fix(a11y): Q.DEFER.004 sub-B part 2 — AdminSupplierPrices use Vuetify label prop + sr-only CSS

Task 4 verification revealed :aria-label НЕ propagates to native <input> через
v-text-field / v-switch. Switch к Vuetify-native :label prop: generates real <label
for=id> element, ассоциация преобразуется в accessible name на input через axe-core.

CSS sr-only hides текст visually (variant='plain' density='compact' preserve).

Tests updated к real-DOM accessibility check (label association, not aria-label attr)."
```

---

### Task 9: Re-verify через Playwright + axe-core

Same protocol as original Task 4, focus только на `/deals` + `/admin/supplier-prices`.

- [ ] **Step 1: Playwright MCP — login → /deals → axe scan**

Expected: `labelViolations: []` (или filter out DevIndexOverlay false-positives `[dev-only DevIndexOverlay]`).

- [ ] **Step 2: → /admin/supplier-prices → axe scan**

Expected: `labelViolations: []`.

- [ ] **Step 3: Update blocked.md Q.DEFER.004 closure block — replace verification block 2026-05-13 с new результатами**

Заменить old «Verification 2026-05-13» block с честным сообщением о failure → новый block с successful re-verification:

```markdown
**Verification 2026-05-13 (part 2, post-Vuetify-workaround):**
- `/deals`: 0 `label` violations critical ✅
- `/admin/supplier-prices`: 0 `label` violations critical ✅
- `/admin/tenants` (sub-C): 1 `aria-tooltip-name` serious — Vuetify-internal, documented (unchanged).

Закрытие реальное: sub-A + sub-B via Vuetify `label` prop + sr-only CSS pattern. Tests updated к accessibility-name DOM check (не `[aria-label]` attribute).
```

- [ ] **Step 4: Commit**

```bash
git add docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md
git commit -m "docs(audit): Q.DEFER.004 — re-verification 2026-05-13 confirms sub-A+B actually closed"
```

---

### Task 10: Memory + push (заменяет original Task 5)

Same protocol as Task 5, дополнительно сохранить **quirk 71** в `memory/feedback_environment.md`:

```
- Quirk 71: Vuetify 3.12 v-checkbox-btn / v-text-field / v-switch **silently drop**
  `aria-label` prop — не propagated на native <input>. Vitest jsdom rendering tolerates
  this (selector `input[aria-label="..."]` somehow matches), но real Chromium DOM показывает
  axe-core critical `label` violation. **Workaround:** use Vuetify `label` prop +
  CSS visually-hidden (.v-label / .v-field-label position absolute clip). Source:
  Q.DEFER.004 Task 4 finding 2026-05-13.
```

Update `MEMORY.md` description line для `feedback_environment.md`:

- было: «...последний bump: post-audit continuation 12.05.2026 ночь, +квирки 69-70...»
- стало: «...последний bump: 13.05.2026 +квирк 71 (Vuetify aria-label drops + Vitest jsdom false-pass)...»

И обновить `memory/project_full_audit_2026-05-12.md` с Q.DEFER.004 фактическим closure status.

Затем `git push origin plan5-frontend-projects`.
