diff --git a/_plugins/search_index_generator.rb b/_plugins/search_index_generator.rb index 59b7d34..9fcb4ba 100644 --- a/_plugins/search_index_generator.rb +++ b/_plugins/search_index_generator.rb @@ -125,6 +125,15 @@ def publish_flat_index(lang_code, menu) end end end + # Learning paths (spec 006): searchable by their title, summary and step titles. + label = lang_code == 'pt-BR' ? 'Trilhas de aprendizado' : 'Learning paths' + (@site.data.dig(lang_code, 'learning-paths') || {}).sort.each do |slug, path| + next unless path.is_a?(Hash) + + steps = Array(path['stages']).flat_map { |stage| Array(stage['steps']).map { |step| step['title'] } }.compact + entries << { 'title' => path['title'], 'url' => "/#{lang_code}/learning-paths/#{slug}/", 'type' => 'learning-path', + 'category' => label, 'description' => [path['summary'], steps.join(' ยท ')].compact.join(' ') } + end topics = topic_lookup(lang_code, menu) seen = {} each_real_resource(@site.data[lang_code], [], [], &lambda { |resource, trail, keys| diff --git a/docs/specs/006-learning-paths/tasks.md b/docs/specs/006-learning-paths/tasks.md index 1ba5c0d..5460157 100644 --- a/docs/specs/006-learning-paths/tasks.md +++ b/docs/specs/006-learning-paths/tasks.md @@ -3,7 +3,7 @@ - [x] T1 Data schema and test (`test/learning_paths_test.rb`) - [x] T2 Generator, layout and index page (en-US, pt-BR) - [x] T3 Interaction script: details, progress, live region; no-JS fallback -- [ ] T4 Subcategory pages list their paths (done); search indexes paths (pending) +- [x] T4 Subcategory pages list their paths; search indexes paths - [x] T5 Author 12 paths per language from IPL data (frontend, backend, Python, JavaScript, data analysis, machine learning, DevOps, cybersecurity, databases, UX, mobile, computer science), with reviews -- [ ] T6 e2e and axe checks +- [x] T6 e2e and axe checks diff --git a/e2e/tests/accessibility.spec.mjs b/e2e/tests/accessibility.spec.mjs index 5764a01..adf1454 100644 --- a/e2e/tests/accessibility.spec.mjs +++ b/e2e/tests/accessibility.spec.mjs @@ -12,6 +12,8 @@ const PAGES = [ ['section', '/en-US/academics/computer-science-theory/cryptography-theory/'], ['wiki', '/en-US/academics/computer-science-theory/cryptography-theory/wiki/'], ['search', '/en-US/search/'], + ['learning paths', '/en-US/learning-paths/'], + ['learning path pt-BR', '/pt-BR/learning-paths/frontend/'], ['not found', '/this-page-does-not-exist/'], ]; diff --git a/e2e/tests/learning-paths.spec.mjs b/e2e/tests/learning-paths.spec.mjs new file mode 100644 index 0000000..2f7764f --- /dev/null +++ b/e2e/tests/learning-paths.spec.mjs @@ -0,0 +1,38 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +// Spec 006: progress per step is kept in the reader's browser and announced; steps work as
. +test('learning path progress persists across reloads and is announced', async ({ page }) => { + await page.goto('/en-US/learning-paths/frontend/'); + const steps = page.locator('.learning-path-step'); + const total = await steps.count(); + expect(total).toBeGreaterThan(5); + + const bar = page.getByRole('progressbar'); + await expect(bar).toHaveAttribute('aria-valuenow', '0'); + + // Only the next unfinished step is open with JavaScript. + await expect(page.locator('.learning-path-step details[open]')).toHaveCount(1); + + const first = steps.first(); + await first.getByRole('checkbox', { name: 'I finished this step' }).check(); + await expect(bar).toHaveAttribute('aria-valuenow', '1'); + await expect(page.getByRole('status')).toContainText(`1 / ${total}`); + + await page.reload(); + await expect(page.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '1'); + // A finished step is collapsed, so read its checkbox directly. + await expect(page.locator('.learning-path-step').first().locator('input[type="checkbox"]')).toBeChecked(); + // After a reload the second step is the next one to open. + await expect(page.locator('.learning-path-step').nth(1).locator('details')).toHaveAttribute('open', ''); +}); + +test('learning path is readable without JavaScript', async ({ browser }) => { + const context = await browser.newContext({ javaScriptEnabled: false }); + const page = await context.newPage(); + await page.goto('/pt-BR/learning-paths/python/'); + const open = page.locator('.learning-path-step details[open]'); + expect(await open.count()).toBe(await page.locator('.learning-path-step').count()); + await expect(page.locator('.learning-path-step a[href^="https://"]').first()).toBeVisible(); + await context.close(); +}); diff --git a/test/build_output_test.rb b/test/build_output_test.rb index edc1ab4..c253db2 100644 --- a/test/build_output_test.rb +++ b/test/build_output_test.rb @@ -34,6 +34,16 @@ def test_subcategory_pages_list_their_curated_resources assert_operator html.scan(/rel="noopener" class="font-semibold text-blue-700/).size, :>=, 10 end + # Spec 006 T4: learning paths are searchable, and their "see also" links are not indexed as resources. + def test_search_index_lists_learning_paths + entries = JSON.parse(SiteBuild.output('api/pt-BR/search-index.json')) + paths = entries.select { |entry| entry['type'] == 'learning-path' } + + assert_operator paths.size, :>=, 12 + assert(paths.any? { |entry| entry['url'] == '/pt-BR/learning-paths/python/' }) + refute(entries.any? { |entry| entry['type'] == 'resource' && entry['url'].start_with?('https://roadmap.sh/') && entry['category'].to_s.include?('See also') }) + end + def test_library_totals_count_every_curated_resource stats = SiteBuild.site.data['resource_stats'] html = SiteBuild.output('en-US/categories/index.html')