@nospt/plugin-dev-ai-hub 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/api/DevAiHubResourceClient.esm.js +97 -0
  2. package/dist/api/DevAiHubResourceClient.esm.js.map +1 -0
  3. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +150 -0
  4. package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -0
  5. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +8 -0
  6. package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js.map +1 -0
  7. package/dist/components/AiResourcesPage/ResourceCard.esm.js +158 -0
  8. package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -0
  9. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +8 -0
  10. package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js.map +1 -0
  11. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +234 -0
  12. package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -0
  13. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +8 -0
  14. package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js.map +1 -0
  15. package/dist/components/AiResourcesPage/ResourceFilters.esm.js +242 -0
  16. package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -0
  17. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js +8 -0
  18. package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js.map +1 -0
  19. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +366 -0
  20. package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -0
  21. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js +8 -0
  22. package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js.map +1 -0
  23. package/dist/components/AiResourcesPage/StatTiles.esm.js +49 -0
  24. package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -0
  25. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +8 -0
  26. package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js.map +1 -0
  27. package/dist/components/AiResourcesPage/index.esm.js +6 -0
  28. package/dist/components/AiResourcesPage/typeMeta.esm.js +35 -0
  29. package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -0
  30. package/dist/components/ToolIcon/ToolIcon.esm.js +26 -9
  31. package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
  32. package/dist/hooks/useResourceBody.esm.js +39 -0
  33. package/dist/hooks/useResourceBody.esm.js.map +1 -0
  34. package/dist/hooks/useResources.esm.js +26 -0
  35. package/dist/hooks/useResources.esm.js.map +1 -0
  36. package/dist/hooks/useTelemetryCounts.esm.js +24 -0
  37. package/dist/hooks/useTelemetryCounts.esm.js.map +1 -0
  38. package/dist/index.d.ts +1 -38
  39. package/dist/index.esm.js +1 -3
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/plugin.esm.js +15 -24
  42. package/dist/plugin.esm.js.map +1 -1
  43. package/dist/routes.esm.js +1 -2
  44. package/dist/routes.esm.js.map +1 -1
  45. package/package.json +17 -10
  46. package/.eslintrc.js +0 -1
  47. package/dev/index.tsx +0 -32
  48. package/dist/api/DevAiHubClient.esm.js +0 -78
  49. package/dist/api/DevAiHubClient.esm.js.map +0 -1
  50. package/dist/components/AssetCard/AssetCard.esm.js +0 -120
  51. package/dist/components/AssetCard/AssetCard.esm.js.map +0 -1
  52. package/dist/components/AssetCard/AssetCard.module.css.esm.js +0 -8
  53. package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +0 -1
  54. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +0 -218
  55. package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +0 -1
  56. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +0 -8
  57. package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +0 -1
  58. package/dist/components/AssetFilters/AssetFilters.esm.js +0 -176
  59. package/dist/components/AssetFilters/AssetFilters.esm.js.map +0 -1
  60. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +0 -8
  61. package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +0 -1
  62. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +0 -138
  63. package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +0 -1
  64. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +0 -8
  65. package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +0 -1
  66. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +0 -195
  67. package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +0 -1
  68. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +0 -8
  69. package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +0 -1
  70. package/dist/components/DevAiHubPage/index.esm.js +0 -6
  71. package/dist/components/McpConfigPage/McpConfigPage.esm.js +0 -200
  72. package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +0 -1
  73. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +0 -8
  74. package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +0 -1
  75. package/dist/components/McpConfigPage/index.esm.js +0 -6
  76. package/dist/components/McpConfigPage/index.esm.js.map +0 -1
  77. package/dist/hooks/index.esm.js +0 -114
  78. package/dist/hooks/index.esm.js.map +0 -1
  79. package/dist/pluginLegacy.esm.js +0 -27
  80. package/dist/pluginLegacy.esm.js.map +0 -1
  81. package/package.json-prepack +0 -85
  82. package/src/api/DevAiHubClient.ts +0 -110
  83. package/src/components/AssetCard/AssetCard.module.css +0 -153
  84. package/src/components/AssetCard/AssetCard.test.tsx +0 -182
  85. package/src/components/AssetCard/AssetCard.tsx +0 -152
  86. package/src/components/AssetCard/index.ts +0 -1
  87. package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +0 -207
  88. package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +0 -313
  89. package/src/components/AssetDetailPanel/index.ts +0 -1
  90. package/src/components/AssetFilters/AssetFilters.module.css +0 -67
  91. package/src/components/AssetFilters/AssetFilters.tsx +0 -219
  92. package/src/components/AssetFilters/index.ts +0 -2
  93. package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +0 -72
  94. package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +0 -176
  95. package/src/components/AssetInstallDialog/index.ts +0 -1
  96. package/src/components/DevAiHubPage/DevAiHubPage.module.css +0 -105
  97. package/src/components/DevAiHubPage/DevAiHubPage.tsx +0 -197
  98. package/src/components/DevAiHubPage/index.ts +0 -1
  99. package/src/components/McpConfigDialog/McpConfigDialog.module.css +0 -111
  100. package/src/components/McpConfigDialog/McpConfigDialog.tsx +0 -271
  101. package/src/components/McpConfigDialog/index.ts +0 -1
  102. package/src/components/McpConfigPage/McpConfigPage.module.css +0 -121
  103. package/src/components/McpConfigPage/McpConfigPage.tsx +0 -242
  104. package/src/components/McpConfigPage/index.ts +0 -1
  105. package/src/components/ToolIcon/ToolIcon.tsx +0 -65
  106. package/src/components/ToolIcon/index.ts +0 -1
  107. package/src/css-modules.d.ts +0 -4
  108. package/src/hooks/index.ts +0 -152
  109. package/src/index.ts +0 -10
  110. package/src/plugin.tsx +0 -60
  111. package/src/pluginLegacy.ts +0 -42
  112. package/src/routes.ts +0 -4
  113. package/tests/e2e/.auth.json +0 -14
  114. package/tests/e2e/asset-detail.spec.ts +0 -90
  115. package/tests/e2e/asset-filtering.spec.ts +0 -145
  116. package/tests/e2e/asset-installation.spec.ts +0 -74
  117. package/tests/e2e/dev-ai-hub-page.spec.ts +0 -89
  118. package/tests/e2e/fixtures/base.ts +0 -61
  119. package/tests/e2e/fixtures/mock-api.ts +0 -221
  120. package/tests/e2e/mcp-config.spec.ts +0 -88
  121. package/tsconfig.json +0 -10
  122. /package/dist/components/{DevAiHubPage → AiResourcesPage}/index.esm.js.map +0 -0
@@ -1,42 +0,0 @@
1
- /**
2
- * Legacy Backstage frontend system support.
3
- *
4
- * For New Frontend System (NFS) use `devAiHubPlugin` from `./plugin` instead.
5
- */
6
- import type { ComponentType } from 'react';
7
- import {
8
- createPlugin,
9
- createRoutableExtension,
10
- createRouteRef,
11
- createApiFactory,
12
- discoveryApiRef,
13
- fetchApiRef,
14
- } from '@backstage/core-plugin-api';
15
- import { devAiHubApiRef, DevAiHubClient } from './api/DevAiHubClient';
16
-
17
- const rootRouteRef = createRouteRef({ id: 'dev-ai-hub' });
18
-
19
- const devAiHubLegacyPlugin = createPlugin({
20
- id: 'dev-ai-hub',
21
- routes: {
22
- root: rootRouteRef,
23
- },
24
- apis: [
25
- createApiFactory({
26
- api: devAiHubApiRef,
27
- deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
28
- factory: ({ discoveryApi, fetchApi }) =>
29
- new DevAiHubClient(discoveryApi, fetchApi),
30
- }),
31
- ],
32
- });
33
-
34
- /** Routable page extension for the legacy frontend system. */
35
- export const DevAiHubPage: ComponentType = devAiHubLegacyPlugin.provide(
36
- createRoutableExtension({
37
- name: 'DevAiHubPage',
38
- component: () =>
39
- import('./components/DevAiHubPage').then(m => m.DevAiHubPage),
40
- mountPoint: rootRouteRef,
41
- }),
42
- );
package/src/routes.ts DELETED
@@ -1,4 +0,0 @@
1
- import { createRouteRef } from '@backstage/frontend-plugin-api';
2
-
3
- export const rootRouteRef = createRouteRef();
4
- export const mcpConfigRouteRef = createRouteRef();
@@ -1,14 +0,0 @@
1
- {
2
- "cookies": [],
3
- "origins": [
4
- {
5
- "origin": "http://localhost:3000",
6
- "localStorage": [
7
- {
8
- "name": "language",
9
- "value": "en"
10
- }
11
- ]
12
- }
13
- ]
14
- }
@@ -1,90 +0,0 @@
1
- import { test, expect } from './fixtures/base';
2
- import { DEV_ASSETS } from './fixtures/mock-api';
3
-
4
- const PAGE_URL = '/dev-ai-hub';
5
-
6
- // First mock asset: TypeScript Best Practices (instruction, claude-code + github-copilot)
7
- const ASSET = DEV_ASSETS[0];
8
-
9
- test.describe('Asset Detail Panel', () => {
10
- test.beforeEach(async ({ page }) => {
11
- await page.goto(PAGE_URL);
12
- await expect(page.getByText(ASSET.name)).toBeVisible();
13
- await page.getByRole('button', { name: 'View details' }).first().click();
14
- });
15
-
16
- test('sets assetId query param in the URL', async ({ page }) => {
17
- await expect(page).toHaveURL(/assetId=/);
18
- });
19
-
20
- test('panel header shows the asset name', async ({ page }) => {
21
- // Scope to the drawer; use first() because the name also appears in the Preview tab <h1>
22
- const panel = page.locator('[role="presentation"] + div');
23
- await expect(panel.getByText(ASSET.name).first()).toBeVisible();
24
- });
25
-
26
- test('panel header shows the asset type tag', async ({ page }) => {
27
- const panel = page.locator('[role="presentation"] + div');
28
- await expect(panel.getByText(ASSET.type)).toBeVisible();
29
- });
30
-
31
- test('panel shows description', async ({ page }) => {
32
- const panel = page.locator('[role="presentation"] + div');
33
- await expect(
34
- panel.getByText('Coding standards and best practices for TypeScript projects.'),
35
- ).toBeVisible();
36
- });
37
-
38
- test('renders Preview, Metadata, and Raw YAML tabs', async ({ page }) => {
39
- for (const name of ['Preview', 'Metadata', 'Raw YAML']) {
40
- await expect(page.getByRole('tab', { name })).toBeVisible();
41
- }
42
- });
43
-
44
- test('Preview tab renders the markdown as HTML', async ({ page }) => {
45
- // dev mock content starts with "# TypeScript Best Practices"
46
- await expect(page.getByRole('heading', { name: 'TypeScript Best Practices' })).toBeVisible();
47
- });
48
-
49
- test('Metadata tab shows author, version, branch', async ({ page }) => {
50
- const panel = page.locator('[role="presentation"] + div');
51
- await page.getByRole('tab', { name: 'Metadata' }).click();
52
- await expect(panel.getByText('Author:')).toBeVisible();
53
- await expect(panel.getByText('Platform Team')).toBeVisible();
54
- await expect(panel.getByText('Version:')).toBeVisible();
55
- await expect(panel.getByText('1.0.0')).toBeVisible();
56
- await expect(panel.getByText('Branch:')).toBeVisible();
57
- await expect(panel.getByText('main')).toBeVisible();
58
- });
59
-
60
- test('Metadata tab shows compatible tools as tags', async ({ page }) => {
61
- await page.getByRole('tab', { name: 'Metadata' }).click();
62
- await expect(page.getByText('Compatible tools')).toBeVisible();
63
- await expect(page.getByText('claude-code')).toBeVisible();
64
- await expect(page.getByText('github-copilot')).toBeVisible();
65
- });
66
-
67
- test('Raw YAML tab shows YAML source', async ({ page }) => {
68
- await page.getByRole('tab', { name: 'Raw YAML' }).click();
69
- await expect(page.getByText(/name: TypeScript Best Practices/)).toBeVisible();
70
- });
71
-
72
- test('"Copy Markdown" button is visible in the actions footer', async ({ page }) => {
73
- await expect(page.getByRole('button', { name: 'Copy Markdown' })).toBeVisible();
74
- });
75
-
76
- test('"Open in Repo" button is visible in the actions footer', async ({ page }) => {
77
- await expect(page.getByRole('button', { name: 'Open in Repo' })).toBeVisible();
78
- });
79
-
80
- test('Close button (×) removes assetId from the URL', async ({ page }) => {
81
- await page.getByRole('button', { name: 'Close' }).click();
82
- await expect(page).not.toHaveURL(/assetId=/);
83
- });
84
-
85
- test('clicking the overlay removes assetId from the URL', async ({ page }) => {
86
- // Overlay has role="presentation"; click dispatches onClose
87
- await page.locator('[role="presentation"]').click({ position: { x: 5, y: 5 } });
88
- await expect(page).not.toHaveURL(/assetId=/);
89
- });
90
- });
@@ -1,145 +0,0 @@
1
- import { test, expect } from './fixtures/base';
2
- import { DEV_ASSETS } from './fixtures/mock-api';
3
-
4
- const PAGE_URL = '/dev-ai-hub';
5
-
6
- test.describe('Asset Filters', () => {
7
- test.beforeEach(async ({ page }) => {
8
- await page.goto(PAGE_URL);
9
- await expect(page.getByText(DEV_ASSETS[0].name)).toBeVisible();
10
- });
11
-
12
- // ── Search ────────────────────────────────────────────────────────────────
13
-
14
- test('SearchField has the correct placeholder text', async ({ page }) => {
15
- await expect(
16
- page.getByRole('searchbox', { name: 'Search assets' }),
17
- ).toHaveAttribute('placeholder', 'Search assets by name, description or content…');
18
- });
19
-
20
- test('searching by partial name filters the grid', async ({ page }) => {
21
- await page.getByRole('searchbox', { name: 'Search assets' }).fill('typescript');
22
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
23
- await expect(page.getByText('Code Review Agent')).not.toBeVisible();
24
- });
25
-
26
- test('search is case-insensitive', async ({ page }) => {
27
- await page.getByRole('searchbox', { name: 'Search assets' }).fill('GIT COMMIT');
28
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
29
- });
30
-
31
- test('search by description matches partial words', async ({ page }) => {
32
- await page.getByRole('searchbox', { name: 'Search assets' }).fill('conventional git commit');
33
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
34
- });
35
-
36
- test('empty search restores all results', async ({ page }) => {
37
- const search = page.getByRole('searchbox', { name: 'Search assets' });
38
- await search.fill('typescript');
39
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
40
-
41
- await search.clear();
42
- for (const asset of DEV_ASSETS) {
43
- await expect(page.getByText(asset.name, { exact: true })).toBeVisible();
44
- }
45
- });
46
-
47
- test('no-match search shows empty state', async ({ page }) => {
48
- await page.getByRole('searchbox', { name: 'Search assets' }).fill('xyznotfound');
49
- await expect(page.getByText('No assets found')).toBeVisible();
50
- });
51
-
52
- // ── Type filter ───────────────────────────────────────────────────────────
53
-
54
- test('all type-filter buttons are rendered', async ({ page }) => {
55
- for (const label of ['All', 'Instructions', 'Agents', 'Skills', 'Workflows']) {
56
- await expect(page.getByRole('button', { name: label }).first()).toBeVisible();
57
- }
58
- });
59
-
60
- test('"Instructions" button filters to instruction assets only', async ({ page }) => {
61
- await page.getByRole('button', { name: 'Instructions', exact: true }).click();
62
- // 2 instruction assets in the mock
63
- await expect(page.getByText('2 assets found')).toBeVisible();
64
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
65
- await expect(page.getByText('Security Guidelines')).toBeVisible();
66
- await expect(page.getByText('Code Review Agent')).not.toBeVisible();
67
- });
68
-
69
- test('"Skills" button filters to skill assets only', async ({ page }) => {
70
- await page.getByRole('button', { name: 'Skills', exact: true }).click();
71
- await expect(page.getByText('1 asset found')).toBeVisible();
72
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
73
- });
74
-
75
- test('"Workflows" button filters to workflow assets only', async ({ page }) => {
76
- await page.getByRole('button', { name: 'Workflows', exact: true }).click();
77
- await expect(page.getByText('1 asset found')).toBeVisible();
78
- await expect(page.getByText('Feature Development Workflow')).toBeVisible();
79
- });
80
-
81
- test('"All" button clears the type filter', async ({ page }) => {
82
- await page.getByRole('button', { name: 'Skills', exact: true }).click();
83
- await expect(page.getByText('1 asset found')).toBeVisible();
84
-
85
- await page.getByRole('button', { name: 'All', exact: true }).click();
86
- await expect(page.getByText(`${DEV_ASSETS.length} assets found`)).toBeVisible();
87
- });
88
-
89
- // ── AI Tool filter ────────────────────────────────────────────────────────
90
-
91
- test('all AI-tool filter buttons are rendered', async ({ page }) => {
92
- for (const label of ['All Tools', 'Claude Code', 'GitHub Copilot', 'Google Gemini', 'Cursor']) {
93
- await expect(page.getByRole('button', { name: label }).first()).toBeVisible();
94
- }
95
- });
96
-
97
- test('"Claude Code" tool filter shows only claude-code-compatible assets', async ({ page }) => {
98
- await page.getByRole('button', { name: 'Claude Code' }).first().click();
99
- // claude-code: mock-1, mock-2, mock-3 (all), mock-5
100
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
101
- await expect(page.getByText('Code Review Agent')).toBeVisible();
102
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible(); // tools: ['all']
103
- await expect(page.getByText('Security Guidelines')).toBeVisible();
104
- await expect(page.getByText('Product Manager')).not.toBeVisible(); // github-copilot only
105
- });
106
-
107
- test('"Cursor" tool filter shows only cursor-compatible assets', async ({ page }) => {
108
- await page.getByRole('button', { name: 'Cursor' }).first().click();
109
- // cursor: mock-4 (github-copilot, cursor), mock-3 (all)
110
- await expect(page.getByText('Feature Development Workflow')).toBeVisible();
111
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible(); // tools: ['all']
112
- });
113
-
114
- // ── Tags ──────────────────────────────────────────────────────────────────
115
-
116
- test('tag chips are rendered for the current result set', async ({ page }) => {
117
- // 'typescript' tag is present in mock-1
118
- await expect(page.getByRole('button', { name: '#typescript' }).first()).toBeVisible();
119
- });
120
-
121
- test('clicking a tag chip filters by that tag', async ({ page }) => {
122
- await page.getByRole('button', { name: '#git' }).first().click();
123
- // mock-3 has ['git', 'commits', 'conventional-commits']
124
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
125
- await expect(page.getByText('TypeScript Best Practices')).not.toBeVisible();
126
- });
127
-
128
- test('clicking a second tag chip adds an AND condition', async ({ page }) => {
129
- await page.getByRole('button', { name: '#typescript' }).first().click();
130
- await page.getByRole('button', { name: '#best-practices' }).first().click();
131
- // Only mock-1 has both 'typescript' AND 'best-practices'
132
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
133
- await expect(page.getByText('Security Guidelines')).not.toBeVisible();
134
- });
135
-
136
- test('clicking an active tag chip deselects it', async ({ page }) => {
137
- const tagBtn = page.getByRole('button', { name: '#typescript' }).first();
138
- await tagBtn.click();
139
- await expect(page.getByText('TypeScript Best Practices')).toBeVisible();
140
- await expect(page.getByText('Code Review Agent')).not.toBeVisible();
141
-
142
- await tagBtn.click(); // deselect
143
- await expect(page.getByText('Code Review Agent')).toBeVisible();
144
- });
145
- });
@@ -1,74 +0,0 @@
1
- import { test, expect } from './fixtures/base';
2
- import { DEV_ASSETS } from './fixtures/mock-api';
3
-
4
- const PAGE_URL = '/dev-ai-hub';
5
-
6
- // First mock asset: TypeScript Best Practices (claude-code + github-copilot)
7
- const ASSET = DEV_ASSETS[0];
8
-
9
- test.describe('Asset Install Dialog', () => {
10
- test.beforeEach(async ({ page }) => {
11
- await page.goto(PAGE_URL);
12
- await expect(page.getByText(ASSET.name)).toBeVisible();
13
- await page.getByRole('button', { name: 'Install in editor' }).first().click();
14
- });
15
-
16
- test('sets installId query param in the URL', async ({ page }) => {
17
- await expect(page).toHaveURL(/installId=/);
18
- });
19
-
20
- test('dialog header shows "Install: <asset-name>"', async ({ page }) => {
21
- // AssetInstallDialog renders "Install: {asset.name}" in DialogHeader
22
- await expect(page.getByText(`Install: ${ASSET.name}`)).toBeVisible();
23
- });
24
-
25
- test('subtitle explains the workflow', async ({ page }) => {
26
- await expect(
27
- page.getByText('Copy the content and place the file at the path shown for your tool.'),
28
- ).toBeVisible();
29
- });
30
-
31
- test('shows a section for each compatible tool', async ({ page }) => {
32
- // ASSET.tools = ['claude-code', 'github-copilot'] — scope to dialog to avoid filter button ambiguity
33
- const dialog = page.getByRole('dialog');
34
- await expect(dialog.getByText('Claude Code', { exact: true })).toBeVisible();
35
- await expect(dialog.getByText('GitHub Copilot', { exact: true })).toBeVisible();
36
- });
37
-
38
- test('each tool section shows an "Install path" label', async ({ page }) => {
39
- const labels = page.getByText('Install path');
40
- await expect(labels.first()).toBeVisible();
41
- });
42
-
43
- test('"Copy Content" button is present', async ({ page }) => {
44
- await expect(page.getByRole('button', { name: 'Copy Content' }).first()).toBeVisible();
45
- });
46
-
47
- test('"Download" button is present', async ({ page }) => {
48
- await expect(page.getByRole('button', { name: 'Download' }).first()).toBeVisible();
49
- });
50
-
51
- test('"Copy Content" button label changes to "Copied!" after click', async ({ page }) => {
52
- await page.getByRole('button', { name: 'Copy Content' }).first().click();
53
- await expect(page.getByRole('button', { name: 'Copied!' }).first()).toBeVisible();
54
- });
55
-
56
- test('"Close" button dismisses the dialog and clears installId', async ({ page }) => {
57
- // Dialog has an X icon button (aria-label="Close") and a footer "Close" text button — use last()
58
- await page.getByRole('button', { name: 'Close' }).last().click();
59
- await expect(page).not.toHaveURL(/installId=/);
60
- });
61
-
62
- // ── Skill asset (tools: all) — verify universal install paths appear ──────
63
-
64
- test('skill asset (tools: all) shows install path for Claude Code', async ({ page }) => {
65
- // Close this dialog and open Git Commit (skill, tools: ['all'])
66
- await page.getByRole('button', { name: 'Close' }).last().click();
67
-
68
- // Git Commit is the 3rd card (index 2) in the mock data order
69
- await page.getByRole('button', { name: 'Install in editor' }).nth(2).click();
70
-
71
- await expect(page.getByText('Install: Git Commit')).toBeVisible();
72
- await expect(page.getByRole('dialog').getByText('Claude Code', { exact: true })).toBeVisible();
73
- });
74
- });
@@ -1,89 +0,0 @@
1
- import { test, expect } from './fixtures/base';
2
- import { DEV_ASSETS, DEV_STATS } from './fixtures/mock-api';
3
-
4
- const PAGE_URL = '/dev-ai-hub';
5
-
6
- test.describe('Dev AI Hub — main page', () => {
7
- test.beforeEach(async ({ page }) => {
8
- await page.goto(PAGE_URL);
9
- // Wait for the asset grid to finish loading
10
- await expect(page.getByText(DEV_ASSETS[0].name)).toBeVisible();
11
- });
12
-
13
- test('renders the four stats cards', async ({ page }) => {
14
- for (const label of ['Instructions', 'Agents', 'Skills', 'Workflows']) {
15
- await expect(page.getByText(label).first()).toBeVisible();
16
- }
17
- });
18
-
19
- test('stats cards display correct counts from mock data', async ({ page }) => {
20
- // The mock has 2 instructions, 2 agents, 1 skill, 1 workflow
21
- const byType = DEV_STATS.byType;
22
- await expect(page.getByText(String(byType.instruction)).first()).toBeVisible();
23
- await expect(page.getByText(String(byType.agent)).first()).toBeVisible();
24
- await expect(page.getByText(String(byType.skill)).first()).toBeVisible();
25
- await expect(page.getByText(String(byType.workflow)).first()).toBeVisible();
26
- });
27
-
28
- test('clicking a stats card filters assets by type', async ({ page }) => {
29
- // Stats cards are plain div[role="button"] (not ToggleButton); they come first in DOM order
30
- await page.getByRole('button', { name: /Skills/i }).first().click();
31
- await expect(page.getByText('1 asset found')).toBeVisible();
32
- await expect(page.getByText('Git Commit', { exact: true })).toBeVisible();
33
- });
34
-
35
- test('clicking an active stats card removes the type filter', async ({ page }) => {
36
- await page.getByRole('button', { name: /Skills/i }).first().click();
37
- await expect(page.getByText('1 asset found')).toBeVisible();
38
-
39
- // Click it again to deselect
40
- await page.getByRole('button', { name: /Skills/i }).first().click();
41
- await expect(page.getByText(`${DEV_STATS.total} assets found`)).toBeVisible();
42
- });
43
-
44
- test('renders all mock assets', async ({ page }) => {
45
- for (const asset of DEV_ASSETS) {
46
- await expect(page.getByText(asset.name, { exact: true })).toBeVisible();
47
- }
48
- });
49
-
50
- test('cards display type labels', async ({ page }) => {
51
- await expect(page.getByText('Instruction').first()).toBeVisible();
52
- await expect(page.getByText('Agent').first()).toBeVisible();
53
- await expect(page.getByText('Skill').first()).toBeVisible();
54
- await expect(page.getByText('Workflow').first()).toBeVisible();
55
- });
56
-
57
- test('popular assets (≥5 installs) show 🔥 icon', async ({ page }) => {
58
- // TypeScript Best Practices (14), Code Review Agent (8), Git Commit (22), Feature Development Workflow (5) are popular
59
- const fireEmojis = page.getByText('🔥');
60
- await expect(fireEmojis.first()).toBeVisible();
61
- });
62
-
63
- test('assets with 0 installs do not show an install count', async ({ page }) => {
64
- // Product Manager has installCount: 0 — the count row is entirely omitted
65
- const card = page.locator('text=Product Manager').locator('..');
66
- await expect(card).not.toContainText('0');
67
- });
68
-
69
- test('"View details" button updates the URL with assetId', async ({ page }) => {
70
- await page.getByRole('button', { name: 'View details' }).first().click();
71
- await expect(page).toHaveURL(/assetId=/);
72
- });
73
-
74
- test('"Install in editor" button updates the URL with installId', async ({ page }) => {
75
- await page.getByRole('button', { name: 'Install in editor' }).first().click();
76
- await expect(page).toHaveURL(/installId=/);
77
- });
78
-
79
- test('shows empty state when no assets match the search', async ({ page }) => {
80
- await page.getByRole('searchbox', { name: 'Search assets' }).fill('zzznoresultszzz');
81
- await expect(page.getByText('No assets found')).toBeVisible();
82
- await expect(page.getByText('Try adjusting your filters or search terms.')).toBeVisible();
83
- });
84
-
85
- test('pagination is hidden when all assets fit one page (PAGE_SIZE=24)', async ({ page }) => {
86
- // 6 mock assets vs PAGE_SIZE=24 — TablePagination is not rendered
87
- await expect(page.getByRole('button', { name: /next/i })).not.toBeVisible();
88
- });
89
- });
@@ -1,61 +0,0 @@
1
- /**
2
- * Pre-seed Backstage's guest-auth localStorage keys so every page load
3
- * auto-signs in as Guest (legacy token path) without a running auth backend.
4
- *
5
- * Keys come from @backstage/core-components SignInPage/providers internals:
6
- * PROVIDER_STORAGE_KEY → '@backstage/core:SignInPage:provider'
7
- * enableLegacyGuestToken (guestProvider.tsx)
8
- *
9
- * API calls to the backend are intercepted via page.route() and served with
10
- * the canonical mock data from mock-api.ts, keeping tests predictable and
11
- * independent of a running backend.
12
- */
13
- import { test as base, expect } from '@playwright/test';
14
- import {
15
- MOCK_ASSETS_FULL,
16
- MOCK_PROVIDER,
17
- MOCK_STATS,
18
- buildListResponse,
19
- } from './mock-api';
20
-
21
- export const test = base.extend<object>({
22
- page: async ({ page }, use) => {
23
- await page.addInitScript(() => {
24
- localStorage.setItem('@backstage/core:SignInPage:provider', 'guest');
25
- localStorage.setItem('enableLegacyGuestToken', 'true');
26
- // Mock clipboard so Copy buttons work without browser permissions
27
- Object.defineProperty(navigator, 'clipboard', {
28
- value: { writeText: async () => {} },
29
- configurable: true,
30
- });
31
- });
32
-
33
- await page.route('**/api/dev-ai-hub/**', async route => {
34
- const url = new URL(route.request().url());
35
- const path = url.pathname.replace(/^.*\/api\/dev-ai-hub/, '');
36
- const method = route.request().method();
37
-
38
- if (method === 'GET' && path === '/assets') {
39
- await route.fulfill({ json: buildListResponse(url.searchParams) });
40
- } else if (method === 'GET' && /^\/assets\/[^/]+$/.test(path)) {
41
- const id = decodeURIComponent(path.split('/')[2]);
42
- const asset = MOCK_ASSETS_FULL.find(a => a.id === id);
43
- if (asset) await route.fulfill({ json: asset });
44
- else await route.fulfill({ status: 404, json: { error: 'Not found' } });
45
- } else if (method === 'GET' && path === '/stats') {
46
- await route.fulfill({ json: MOCK_STATS });
47
- } else if (method === 'GET' && path === '/providers') {
48
- await route.fulfill({ json: [MOCK_PROVIDER] });
49
- } else if (method === 'POST' && path.endsWith('/track-install')) {
50
- await route.fulfill({ status: 204, body: '' });
51
- } else {
52
- await route.continue();
53
- }
54
- });
55
-
56
- // eslint-disable-next-line react-hooks/rules-of-hooks
57
- await use(page);
58
- },
59
- });
60
-
61
- export { expect };