Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions app/components/RunDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ defineProps<{
error: string | null
loading: boolean
onClear: () => void
currentReportId?: string | null
}>()

const generateReportPermalink = (reportId: string) => {
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
return `${baseUrl}/report/${reportId}`
}

const copyToClipboard = async (text: string) => {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
try {
await navigator.clipboard.writeText(text)
}
catch (err) {
console.error('Failed to copy to clipboard:', err)
}
}
}
</script>

<template>
Expand Down Expand Up @@ -35,6 +52,27 @@ defineProps<{
</div>

<div class="reset-container">
<div
v-if="currentReportId && runs.length > 0"
class="report-permalink"
>
<label>Share this report:</label>
<div class="permalink-container">
<input
:value="generateReportPermalink(currentReportId)"
readonly
class="permalink-input"
/>
Comment on lines +61 to +65
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be much wider as it often contains quite long URLs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b38cbb7 - changed max-width from 600px to 100% so the input can expand to show longer URLs.

<button
class="copy-button"
title="Copy to clipboard"
@click="copyToClipboard(generateReportPermalink(currentReportId))"
>
📋
</button>
Comment on lines +66 to +72
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some reason this isn't properly vertically aligned with the input

Image

I also find the blue background rather ugly. must just a sleek blue border instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b38cbb7:

  • Changed align-items from center to stretch to ensure proper vertical alignment
  • Replaced blue background with white background and 2px blue border (border: 2px solid var(--blue-600))
  • Added display: flex with align-items: center on button for consistent internal alignment
  • On hover, button now shows light blue background (var(--blue-50)) instead of darker blue

</div>
</div>

<button
v-if="runs.length > 0"
@click="onClear()"
Expand Down Expand Up @@ -70,4 +108,54 @@ defineProps<{
text-align: center;
background-color: inherit;
}

.report-permalink {
margin-bottom: 1em;
padding: 1em;
background-color: var(--bg-200, #f8fafc);
border-radius: 0.5em;
border: 1px solid var(--border-200, #e2e8f0);
}

.report-permalink label {
display: block;
font-weight: 500;
margin-bottom: 0.5em;
color: var(--text-700, #374151);
}

.permalink-container {
display: flex;
gap: 0.5em;
align-items: stretch;
max-width: 100%;
margin: 0 auto;
}

.permalink-input {
flex: 1;
padding: 0.5em;
border: 1px solid var(--border-300, #d1d5db);
border-radius: 0.25em;
background-color: white;
font-family: monospace;
font-size: 0.875em;
}

.copy-button {
padding: 0.5em 1em;
background-color: white;
color: var(--blue-600, #2563eb);
border: 2px solid var(--blue-600, #2563eb);
border-radius: 0.25em;
cursor: pointer;
font-size: 0.875em;
display: flex;
align-items: center;
justify-content: center;
}

.copy-button:hover {
background-color: var(--blue-50, #eff6ff);
}
</style>
125 changes: 125 additions & 0 deletions app/composables/useRunManager.report.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useRunManager } from './useRunManager'
import type { ApiRun } from '~/types/run'

// Mock the getCacheHeaders function
vi.mock('~/utils/getCacheHeaders', () => ({
default: vi.fn((headers: Record<string, string>) => headers),
}))

// Mock navigateTo composable
vi.mock('#app/composables/router', () => ({
navigateTo: vi.fn(async () => {}),
}))

// Mock fetch and $fetch
global.fetch = vi.fn()
vi.stubGlobal('$fetch', vi.fn())

describe('useRunManager - Report Functionality', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('tracks currentReportId state', () => {
const { currentReportId, setCurrentReportId } = useRunManager()

expect(currentReportId.value).toBe(null)

setCurrentReportId('test-report-123')
expect(currentReportId.value).toBe('test-report-123')

setCurrentReportId(null)
expect(currentReportId.value).toBe(null)
})

it('sends currentReportId in API requests when set', async () => {
const mockApiRun: ApiRun = {
runId: 'test-run',
url: 'https://example.com',
status: 200,
durationInMs: 100,
headers: { 'cache-control': 'max-age=3600' },
reportId: 'new-report-456',
}

const mockFetch = $fetch as unknown as ReturnType<typeof vi.fn>
mockFetch.mockResolvedValueOnce(mockApiRun)

const { handleRequestFormSubmit, setCurrentReportId, currentReportId } = useRunManager()

setCurrentReportId('existing-report-123')

await handleRequestFormSubmit({ url: 'https://example.com' })

expect(mockFetch).toHaveBeenCalledWith('/api/inspect-url', {
method: 'POST',
body: {
url: 'https://example.com',
currentReportId: 'existing-report-123',
},
})

// Should update to the new report ID returned from API
expect(currentReportId.value).toBe('new-report-456')
})

it('updates currentReportId when API returns new reportId', async () => {
const mockApiRun: ApiRun = {
runId: 'test-run',
url: 'https://example.com',
status: 200,
durationInMs: 100,
headers: { 'cache-control': 'max-age=3600' },
reportId: 'new-report-789',
}

const mockFetch = $fetch as unknown as ReturnType<typeof vi.fn>
mockFetch.mockResolvedValueOnce(mockApiRun)

const { handleRequestFormSubmit, currentReportId } = useRunManager()

expect(currentReportId.value).toBe(null)

await handleRequestFormSubmit({ url: 'https://example.com' })

expect(currentReportId.value).toBe('new-report-789')
})

it('clears currentReportId when clearing runs', async () => {
const { handleClickClear, setCurrentReportId, currentReportId } = useRunManager()

setCurrentReportId('test-report-123')
expect(currentReportId.value).toBe('test-report-123')

await handleClickClear()

expect(currentReportId.value).toBe(null)
})

it('handles API response without reportId', async () => {
const mockApiRun: ApiRun = {
runId: 'test-run',
url: 'https://example.com',
status: 200,
durationInMs: 100,
headers: { 'cache-control': 'max-age=3600' },
// No reportId in response
}

const mockFetch = $fetch as unknown as ReturnType<typeof vi.fn>
mockFetch.mockResolvedValueOnce(mockApiRun)

const { handleRequestFormSubmit, currentReportId, setCurrentReportId } = useRunManager()

setCurrentReportId('existing-report')

await handleRequestFormSubmit({ url: 'https://example.com' })

// Should keep existing reportId if API doesn't return one
expect(currentReportId.value).toBe('existing-report')
})
})
22 changes: 15 additions & 7 deletions app/composables/useRunManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ vi.mock('~/utils/getCacheHeaders', () => ({
default: vi.fn((headers: Record<string, string>) => headers),
}))

// Mock navigateTo composable
vi.mock('#app/composables/router', () => ({
navigateTo: vi.fn(async () => {}),
}))

// Mock fetch and $fetch
global.fetch = vi.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
global.$fetch = vi.fn() as any
vi.stubGlobal('$fetch', vi.fn())

describe('useRunManager', () => {
beforeEach(() => {
Expand All @@ -37,6 +41,7 @@ describe('useRunManager', () => {
status: 200,
durationInMs: 100,
headers: { 'cache-control': 'max-age=3600' },
reportId: 'test-report',
}

const run = getRunFromApiRun(apiRun)
Expand All @@ -47,6 +52,7 @@ describe('useRunManager', () => {
status: 200,
durationInMs: 100,
cacheHeaders: { 'cache-control': 'max-age=3600' },
reportId: 'test-report',
})
})

Expand All @@ -57,10 +63,10 @@ describe('useRunManager', () => {
status: 200,
durationInMs: 100,
headers: { 'cache-control': 'max-age=3600' },
reportId: 'test-report',
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockFetch = vi.mocked($fetch as any)
const mockFetch = $fetch as unknown as ReturnType<typeof vi.fn>
mockFetch.mockResolvedValueOnce(mockApiRun)

const { runs, error, loading, handleRequestFormSubmit } = useRunManager()
Expand All @@ -73,13 +79,15 @@ describe('useRunManager', () => {
expect(runs.value[0]?.url).toBe('https://example.com')
expect(mockFetch).toHaveBeenCalledWith('/api/inspect-url', {
method: 'POST',
body: { url: 'https://example.com' },
body: {
url: 'https://example.com',
currentReportId: null,
},
})
})

it('handles API request error', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockFetch = vi.mocked($fetch as any)
const mockFetch = $fetch as unknown as ReturnType<typeof vi.fn>
mockFetch.mockRejectedValueOnce(new Error('HTTP 500'))

const { runs, error, loading, handleRequestFormSubmit } = useRunManager()
Expand Down
25 changes: 23 additions & 2 deletions app/composables/useRunManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const useRunManager = () => {
const runs = ref<Run[]>([])
const error = ref<string | null>(null)
const loading = ref<boolean>(false)
const currentReportId = ref<string | null>(null)

const getRunFromApiRun = (apiRun: ApiRun): Run => {
const { headers, ...run } = apiRun
Expand All @@ -19,10 +20,21 @@ export const useRunManager = () => {
try {
const responseBody: ApiRun = await $fetch<ApiRun>('/api/inspect-url', {
method: 'POST',
body: { url },
body: {
url,
currentReportId: currentReportId.value,
},
})

runs.value.push(getRunFromApiRun(responseBody))

// Update current report ID with the new one returned from the API
if (responseBody.reportId) {
currentReportId.value = responseBody.reportId
// Navigate to the report URL to make it the source of truth
await navigateTo(`/report/${responseBody.reportId}`)
}

error.value = null
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -38,8 +50,11 @@ export const useRunManager = () => {
}
}

const handleClickClear = (): void => {
const handleClickClear = async (): Promise<void> => {
runs.value = []
currentReportId.value = null
// Navigate back to home
await navigateTo('/')
}

const addRun = (run: Run): void => {
Expand All @@ -54,18 +69,24 @@ export const useRunManager = () => {
error.value = newError
}

const setCurrentReportId = (reportId: string | null): void => {
currentReportId.value = reportId
}

return {
// State
runs: readonly(runs),
error: readonly(error),
loading: readonly(loading),
currentReportId: readonly(currentReportId),

// Methods
handleRequestFormSubmit,
handleClickClear,
addRun,
setRuns,
setError,
setCurrentReportId,
getRunFromApiRun,
}
}
3 changes: 2 additions & 1 deletion app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const { runs, error, loading, handleRequestFormSubmit, handleClickClear } = useRunManager()
const { runs, error, loading, handleRequestFormSubmit, handleClickClear, currentReportId } = useRunManager()
</script>

<template>
Expand All @@ -14,6 +14,7 @@ const { runs, error, loading, handleRequestFormSubmit, handleClickClear } = useR
:error="error"
:loading="loading"
:on-clear="handleClickClear"
:current-report-id="currentReportId"
/>
</main>
</template>
Expand Down
Loading