diff --git a/docs/QA-CHECKLIST.md b/docs/QA-CHECKLIST.md new file mode 100644 index 0000000..4271ef2 --- /dev/null +++ b/docs/QA-CHECKLIST.md @@ -0,0 +1,296 @@ +# WP Multi City — Manual QA Checklist + +End-to-end verification against a real WordPress installation. Run through every section before any release. Each section maps to a specific acceptance criterion from the MVP epic (`cp-cw4`). + +**Test environment:** +- WordPress 6.0+ (clean install, no other plugins beyond ACF Pro) +- PHP 8.0+ +- Advanced Custom Fields Pro 6.0+ activated + +**Tester:** ______________ **Date:** ______________ **Plugin version:** ______________ + +--- + +## 1. Plugin activation + +**cp-cw4 acceptance:** "Плагин активируется на чистом WP с ACF Pro" + +Preconditions: clean WP 6.0+ with activated ACF Pro. + +Steps: +1. Copy plugin to `wp-content/plugins/wp-multi-city/`. +2. Go to admin → Plugins → activate "WP Multi City". + +Expected: +- No PHP errors in `debug.log` (with `WP_DEBUG=true`). +- No admin notice about missing ACF Pro. +- Admin menu shows: "Шорткоды" (top-level), "WP Multi City" (top-level). + +- [ ] Section pass + +--- + +## 2. ACF dependency notice + +Preconditions: WP with ACF Pro **deactivated**. + +Steps: +1. Deactivate ACF Pro. +2. Refresh admin. + +Expected: +- Admin notice visible: "WP Multi City: требуется активный плагин Advanced Custom Fields Pro". +- No "Шорткоды" or "WP Multi City" menu items (plugin self-disabled). + +After verification, reactivate ACF Pro for the rest of the checklist. + +- [ ] Section pass + +--- + +## 3. Taxonomy `town` admin UI + +**cp-cw4 acceptance:** "В админке создаётся taxonomy town" + +Preconditions: §1 passed. + +Steps: +1. Open admin → Шорткоды → Города. +2. Create term with slug `spb`, name "Санкт-Петербург". +3. Create term with slug `tyumen`, name "Тюмень". + +Expected: +- Both terms appear in the list. +- No "Hierarchical" parent-picker (taxonomy is flat). +- No public archive page accessible at `/town/spb/` (404 expected — taxonomy is non-public). + +- [ ] Section pass + +--- + +## 4. CPT `shortcode-town` admin UI + +**cp-cw4 acceptance:** "В админке создаётся CPT shortcode-town" + +Preconditions: §3 passed. + +Steps: +1. Open admin → Шорткоды → Добавить. +2. Observe the edit screen. +3. Set title "Shortcode #1", save as Published (or Draft). +4. Note the post ID (e.g., 42) from the URL. + +Expected: +- Edit screen shows: title field + ACF repeater "Подстановки по городам" (`list_shortcode`) with two sub-fields "Город" (taxonomy dropdown of town terms) + "Значение" (textarea). +- No editor / content / featured-image fields (CPT `supports=['title']` only). +- Post saves successfully. + +- [ ] Section pass + +--- + +## 5. Options page UI + +Preconditions: §3 passed. + +Steps: +1. Open admin → WP Multi City. + +Expected: +- ACF options page renders with 4 fields: + - `main_town_slug` — taxonomy dropdown of town terms. + - `index_town_alt` — page picker. + - `page_unic` — repeater with 3 sub-fields (page_old, page_new, town). + - `page_no_rep` — repeater with 1 sub-field (link). +- Set `main_town_slug = spb` and save. + +- [ ] Section pass + +--- + +## 6. Helper functions smoke + +**cp-cw4 acceptance:** "Шаблоны темы могут вызвать current_town(), _alt_get_field()" + +Preconditions: §5 passed. Create page "About" via admin → Pages → Add New, slug `about`, note ID (e.g., 10). + +Steps: +1. Add this to `wp-content/themes//functions.php` (or as a mu-plugin): + + ```php + add_action('wp_footer', function () { + error_log('wpmc_current_town: ' . var_export(wpmc_current_town(), true)); + error_log('wpmc_remove_domain_link: ' . wpmc_remove_domain_link('https://example.com/foo/')); + $term = wpmc_get_term_by_slug('tyumen'); + error_log('wpmc_get_term_by_slug: ' . ($term ? $term->slug : 'null')); + }); + ``` + +2. Open `/tyumen/about/` (front-end). Check `wp-content/debug.log`. +3. Open `/` (front-end). Check log again. + +Expected: +- `/tyumen/about/` log: `wpmc_current_town: 'tyumen'`, `wpmc_remove_domain_link: '/foo/'`, `wpmc_get_term_by_slug: 'tyumen'`. +- `/` log: `wpmc_current_town: 'spb'` (from main_town_slug option fallback). + +Clean up: remove the debug snippet from functions.php. + +- [ ] Section pass + +--- + +## 7. `[shortcode_dmr]` resolves by current town + +**cp-cw4 acceptance:** "[shortcode_dmr id=N] подменяется на значение для текущего города" + +Preconditions: §6 passed. Open the shortcode-town post created in §4 (ID=42). Add two rows to `list_shortcode`: +- Row 1: Город = spb, Значение = `СПб-текст` +- Row 2: Город = tyumen, Значение = `Тюмень-текст` + +Save the shortcode-town post. + +Steps: +1. Create page "Test", slug `test`, content: `[shortcode_dmr id=42]`. Publish. +2. Open `/tyumen/test/`. +3. Open `/test/` (no city prefix). +4. Open `/nonexistent-city/test/` (no matching term). + +Expected: +- `/tyumen/test/` displays `Тюмень-текст`. +- `/test/` displays `СПб-текст` (current_town falls back to main_town_slug=spb). +- `/nonexistent-city/test/` displays `СПб-текст` (first segment not a town slug → main town fallback) or empty depending on permalink resolution. + +- [ ] Section pass + +--- + +## 8. URL router patches `$wp_query` + +**cp-cw4 acceptance:** "На /{city-slug}/{page-slug}/ открывается соответствующая страница" + +Preconditions: §6 passed (page About ID=10 exists). §5 with main_town_slug=spb. + +Steps: +1. Open `/tyumen/about/`. +2. View page source (or template debug output) — check `get_queried_object_id()` value. +3. Open `/spb/about/`. + +Expected: +- `/tyumen/about/` loads page About (id=10) content; `get_queried_object_id()` = 10. +- In a template, `get_query_var('town')` returns `'tyumen'`. +- `/spb/about/` returns 404 OR loads About — main town routing is a no-op (Router skips), so default WP rewrite rules govern. + +- [ ] Section pass + +--- + +## 9. `/{slug}/` root with `main_page_alt` + +Preconditions: §8 passed. Create page "Главная Тюмень", slug `glavnaya-tyumen`, note ID (e.g., 20). On the `tyumen` term (admin → Шорткоды → Города → tyumen → Edit), set ACF field `main_page_alt` = 20. Save term. + +Steps: +1. Open `/tyumen/`. + +Expected: +- Page Главная Тюмень (id=20) loads. +- `is_front_page()` returns true in template. +- `get_queried_object_id()` returns 20. + +- [ ] Section pass + +--- + +## 10. `page_unic` clone swap + +**cp-cw4 acceptance:** "На /{city-slug}/{page-slug}/ открывается соответствующая страница" (with clone) + +Preconditions: §8 passed (page About ID=10 exists). Create clone page "About-Тюмень", slug `about-tyumen`, ID=21, content "Тюменский About-content". + +In admin → WP Multi City → page_unic, add one row: page_old=10 (About), page_new=21 (About-Тюмень), town=tyumen. Save options. + +Steps: +1. Open `/tyumen/about/`. +2. Inspect page content. + +Expected: +- URL bar stays `/tyumen/about/`. +- Content rendered is "Тюменский About-content" (from page 21, the clone). +- `get_queried_object_id()` returns 21 (not 10). + +- [ ] Section pass + +--- + +## 11. URL filters — town prefix injection + +**cp-cw4 acceptance:** (implied) Links in templates auto-prefix with city slug. + +Preconditions: §8 passed. The active theme renders a primary navigation menu. Add menu items: link to `/uslugi/` (custom link), link to page About. + +Steps: +1. Open `/tyumen/about/`. +2. Inspect HTML of menu items (view source). Inspect any ACF link field used by the theme. Add a template snippet that outputs `home_url('/contact/')` and `home_url('/tyumen/contact/')` separately. +3. Open `/` (main town context). + +Expected on `/tyumen/about/`: +- Custom menu link `/uslugi/` rendered as `https:///tyumen/uslugi/` (or `/tyumen/uslugi/`). +- About menu link (originally `/about/`) rendered with `/tyumen/` prefix. +- `home_url('/contact/')` returns `https:///tyumen/contact/`. +- `home_url('/tyumen/contact/')` returns `https:///tyumen/contact/` (no double-prefix). +- ACF link field `/internal/` value rendered as `/tyumen/internal/`. +- mailto: and tel: links unchanged. +- External `https://other-domain.com/` links unchanged. + +Expected on `/`: +- All URLs unchanged (current_town == main_town_slug → no prefix). + +- [ ] Section pass + +--- + +## 12. `page_no_rep` exclusions + +Preconditions: §11 passed. In admin → WP Multi City → page_no_rep, add row: link = `/privacy/`. Save. Create page "Privacy", slug `privacy`, ID=30. + +Steps: +1. Open `/privacy/`. +2. Open `/tyumen/privacy/`. +3. Check menu/template link to `/privacy/` from a `/tyumen/...` context. + +Expected: +- `/privacy/` loads page Privacy normally, no city routing. +- `/tyumen/privacy/` — Router does not patch `$wp_query` (path excluded); WP either 404s or falls back depending on rewrite rules. +- A link to `/privacy/` rendered from `/tyumen/about/` stays `/privacy/` (NOT prefixed to `/tyumen/privacy/`). + +- [ ] Section pass + +--- + +## 13. REST API URL + +**cp-cw4 acceptance:** (implied) REST API URL correct under host alias / proxy. + +Preconditions: REST API enabled (default). + +Steps: +1. In a template, add: ``. Render any front-end page. +2. Open the printed URL in browser or via curl. + +Expected: +- Rendered URL: `https:///wp-json/wp/v2/posts` (HTTPS forced, HTTP_HOST used). +- HTTP request returns JSON array of posts (200 OK). +- If `HTTP_HOST` differs from `site_url()`'s host, the rendered URL prefers `HTTP_HOST`. + +- [ ] Section pass + +--- + +## Release sign-off + +- [ ] All 13 sections passed +- [ ] PHPUnit suite green (`vendor/bin/phpunit` → OK 98 tests, 175 assertions) +- [ ] PHP lint clean (`for f in includes/*.php; do php -l "$f"; done` → "No syntax errors detected" for every file) +- [ ] Tested on WP 6.0+ with PHP 8.0+ +- [ ] No PHP notices/warnings in `wp-content/debug.log` + +**Tester signature:** ______________ **Date:** ______________