Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1dbb4b148
|
||
|
|
cab6f79255
|
||
|
|
a7cca3162d
|
||
|
|
955f310539 | ||
|
|
6e1142553b
|
No files matched your search
@@ -238,7 +238,7 @@ function mockAssistant(options: { config?: Record<string, unknown>; list?: Assis
|
||||
*/
|
||||
async function openRail(user: ReturnType<typeof userEvent.setup>) {
|
||||
await user.click(await screen.findByRole('button', { name: /^Ask the assistant/ }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Show conversations' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Conversation history' }))
|
||||
await screen.findByRole('button', { name: UNREAD_ROW })
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('conversation list in the panel', () => {
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await user.click(await screen.findByRole('button', { name: /^Ask the assistant/ }))
|
||||
const toggle = await screen.findByRole('button', { name: 'Show conversations' })
|
||||
const toggle = await screen.findByRole('button', { name: 'Conversation history' })
|
||||
await waitFor(() => expect(toggle.querySelector('[data-qqq-id="assistant-rail-toggle-dot"]')).not.toBeNull())
|
||||
expect(toggle).toHaveAccessibleDescription('Other conversations are working or have unread replies')
|
||||
})
|
||||
@@ -357,7 +357,7 @@ describe('conversation list in the panel', () => {
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await user.click(await screen.findByRole('button', { name: /^Ask the assistant/ }))
|
||||
const toggle = await screen.findByRole('button', { name: 'Show conversations' })
|
||||
const toggle = await screen.findByRole('button', { name: 'Conversation history' })
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
expect(toggle.querySelector('[data-qqq-id="assistant-rail-toggle-dot"]')).toBeNull()
|
||||
})
|
||||
@@ -404,7 +404,7 @@ describe('unread correctness', () => {
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await user.click(await screen.findByRole('button', { name: /^Ask the assistant/ }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Show conversations' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Conversation history' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Quiet one' }))
|
||||
const composer = await screen.findByRole('textbox', { name: 'Message the assistant' })
|
||||
await user.type(composer, 'Hello{Enter}')
|
||||
@@ -420,7 +420,7 @@ describe('unread correctness', () => {
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await user.click(await screen.findByRole('button', { name: /^Ask the assistant, 2 unread/ }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Show conversations' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Conversation history' }))
|
||||
await user.click(await screen.findByRole('button', { name: UNREAD_ROW }))
|
||||
await waitFor(() => expect(traffic.patches).toContainEqual({ id: '2', body: { read: true } }))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @file AssistantHeaderTip — hover and keyboard-focus tooltip for the assistant panel's icon buttons.
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
/**
|
||||
* Wraps one icon button so its label shows as a tooltip on hover and on keyboard focus. The button
|
||||
* keeps its own `aria-label`; the tooltip is a visual duplicate for sighted users.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @param props.label - Tooltip text.
|
||||
* @param props.children - The single button element.
|
||||
* @returns The button with a tooltip.
|
||||
*/
|
||||
export function HeaderTip({ label, children }: { label: string; children: React.ReactElement }) {
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={300}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content sideOffset={4} className="z-[var(--qqq-z-tooltip,3000)] rounded bg-popover px-2 py-1 text-xs text-popover-foreground shadow" data-qqq-id="assistant-tooltip">
|
||||
{label}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ describe('answer lifecycle and recovery', () => {
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await openComposer(user)
|
||||
await user.click(screen.getByRole('button', { name: 'Show conversations' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Conversation history' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Saved conversation' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'Stop the reply' }))
|
||||
expect(await screen.findByText('Finished on another pod', { selector: MESSAGE_TEXT })).toBeInTheDocument()
|
||||
@@ -755,3 +755,72 @@ describe('multiple run controls', () => {
|
||||
await waitFor(() => expect(screen.queryByText('Writing...')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
describe('panel header chrome and starter prompts', () => {
|
||||
const LONG = 'Which of my open orders have been waiting on a customer reply for more than two weeks, and what was the last thing we said to them?'
|
||||
|
||||
it('labels the history, new and close buttons with an icon, aria-label and keyboard-focus tooltip', async () => {
|
||||
mockAssistant()
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await openComposer(user)
|
||||
const panel = await screen.findByRole('dialog', {}, { timeout: 10_000 })
|
||||
const history = within(panel).getByRole('button', { name: 'Conversation history' })
|
||||
expect(history).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(history.querySelector('svg')).toHaveClass('lucide-history')
|
||||
expect(within(panel).getByRole('button', { name: 'New conversation' })).toBeInTheDocument()
|
||||
expect(within(panel).getByRole('button', { name: 'Close assistant' })).toBeInTheDocument()
|
||||
|
||||
// keyboard focus alone (no hover) opens the tooltip
|
||||
act(() => history.focus())
|
||||
expect(await screen.findByRole('tooltip', {}, { timeout: 3000 })).toHaveTextContent('Conversation history')
|
||||
await user.click(history)
|
||||
expect(within(panel).getByRole('button', { name: 'Hide conversation history' })).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('wraps long starter prompts inside the chip and stacks them, with no truncation', async () => {
|
||||
mockAssistant({ enabled: true, profile: { starterPrompts: [LONG, 'Short one', LONG + ' Again?'] } })
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await openComposer(user)
|
||||
const list = await screen.findByRole('list', { name: 'Suggested questions' })
|
||||
const chips = within(list).getAllByRole('button')
|
||||
expect(chips).toHaveLength(3)
|
||||
for (const chip of chips) {
|
||||
expect(chip).toHaveClass('whitespace-normal', 'break-words', 'w-full')
|
||||
expect(chip.className).not.toMatch(/truncate|whitespace-nowrap|overflow-hidden/)
|
||||
expect(chip.parentElement).toHaveClass('w-full', 'min-w-0', '@md:w-auto')
|
||||
}
|
||||
expect(chips[0]).toHaveTextContent(LONG)
|
||||
})
|
||||
|
||||
it.each([[0], [1], [2], [3]])('renders %i starter prompts without an empty list', async (count) => {
|
||||
mockAssistant({ enabled: true, profile: { starterPrompts: Array.from({ length: count }, (_, index) => `Question ${index + 1}`) } })
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await openComposer(user)
|
||||
await screen.findByText('Ask about your data')
|
||||
const list = screen.queryByRole('list', { name: 'Suggested questions' })
|
||||
if (count === 0) expect(list).not.toBeInTheDocument()
|
||||
else expect(within(list as HTMLElement).getAllByRole('button')).toHaveLength(count)
|
||||
})
|
||||
|
||||
it('has no axe violations: empty state, rail open, and with a conversation', async () => {
|
||||
mockAssistant({ enabled: true, profile: { starterPrompts: [LONG, 'Short one'] } })
|
||||
server.use(http.get(`${BASE}/conversations`, () => HttpResponse.json({ conversations: [{ ...CONVERSATION, title: 'Saved conversation' }] })))
|
||||
const user = userEvent.setup()
|
||||
renderShell()
|
||||
await openComposer(user)
|
||||
const panel = await screen.findByRole('dialog', {}, { timeout: 10_000 })
|
||||
expect(await axe(panel)).toHaveNoViolations()
|
||||
|
||||
await user.click(within(panel).getByRole('button', { name: 'Conversation history' }))
|
||||
await screen.findByRole('button', { name: 'Saved conversation' })
|
||||
expect(await axe(panel)).toHaveNoViolations()
|
||||
|
||||
persisted.messages = storedTurn('Find records', [{ type: 'text', text: 'Here they are.' }])
|
||||
await user.click(screen.getByRole('button', { name: 'Saved conversation' }))
|
||||
expect(await screen.findByText('Here they are.', { selector: MESSAGE_TEXT })).toBeInTheDocument()
|
||||
expect(await axe(panel)).toHaveNoViolations()
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Archive, ArchiveRestore, Check, Eye, MoreHorizontal, PanelLeft, Pencil, PencilLine, Pin, PinOff, SquarePen, Trash2, X } from 'lucide-react'
|
||||
import { Archive, ArchiveRestore, Check, Eye, History, MoreHorizontal, Pencil, PencilLine, Pin, PinOff, SquarePen, Trash2, X } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { QInstance } from '@/types'
|
||||
@@ -48,6 +48,7 @@ import { cn } from '@/lib/utils/cn'
|
||||
import { ASSISTANT_DOCKED_MEDIA_QUERY, assistantKeys, useAssistant, type AssistantController } from './assistant-context'
|
||||
import { AssistantElapsed } from './AssistantRunStatus'
|
||||
import { AssistantComposer } from './AssistantComposer'
|
||||
import { HeaderTip } from './AssistantHeaderTip'
|
||||
import { AssistantMessageView } from './AssistantMessageView'
|
||||
import { ConversationRail, conversationTitle } from './ConversationRail'
|
||||
import { markConversationRead } from './mark-read'
|
||||
@@ -315,17 +316,18 @@ function PanelBody({ assistant, engine, wide, titleId, composerRef }: PanelBodyP
|
||||
return (
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<div className="flex shrink-0 items-center gap-1 border-b border-border px-2" style={{ minHeight: 'var(--qqq-header-height)' }}>
|
||||
<HeaderTip label={railOpen ? 'Hide conversation history' : 'Conversation history'}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(iconButton, 'relative')}
|
||||
onClick={() => setRailOpen((value) => !value)}
|
||||
aria-expanded={railOpen}
|
||||
aria-controls={listId}
|
||||
aria-label={railOpen ? 'Hide conversations' : 'Show conversations'}
|
||||
aria-label={railOpen ? 'Hide conversation history' : 'Conversation history'}
|
||||
aria-describedby={othersBusy ? `${listId}-busy` : undefined}
|
||||
data-qqq-id="assistant-rail-toggle"
|
||||
>
|
||||
<PanelLeft className="h-4 w-4" aria-hidden="true" />
|
||||
<History className="h-4 w-4" aria-hidden="true" />
|
||||
{othersBusy && (
|
||||
<>
|
||||
<span className="absolute right-0.5 top-0.5 h-2 w-2 rounded-full bg-primary" aria-hidden="true" data-qqq-id="assistant-rail-toggle-dot" />
|
||||
@@ -333,6 +335,7 @@ function PanelBody({ assistant, engine, wide, titleId, composerRef }: PanelBodyP
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</HeaderTip>
|
||||
{renaming && conversation ? (
|
||||
<form
|
||||
className="min-w-0 flex-1"
|
||||
@@ -420,6 +423,7 @@ function PanelBody({ assistant, engine, wide, titleId, composerRef }: PanelBodyP
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
)}
|
||||
<HeaderTip label="New conversation">
|
||||
<button
|
||||
type="button"
|
||||
className={iconButton}
|
||||
@@ -434,9 +438,12 @@ function PanelBody({ assistant, engine, wide, titleId, composerRef }: PanelBodyP
|
||||
>
|
||||
<SquarePen className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
<button type="button" className={iconButton} onClick={() => assistant.setOpen(false)} aria-label="Close assistant" data-qqq-id="assistant-close">
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</HeaderTip>
|
||||
<HeaderTip label="Close assistant">
|
||||
<button type="button" className={iconButton} onClick={() => assistant.setOpen(false)} aria-label="Close assistant" data-qqq-id="assistant-close">
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</HeaderTip>
|
||||
</div>
|
||||
|
||||
{Object.entries(engine.runs).filter(([id]) => (Number(id) !== activeConversationId || !showMessages)).map(([id, runs]) => {
|
||||
@@ -495,11 +502,11 @@ function PanelBody({ assistant, engine, wide, titleId, composerRef }: PanelBodyP
|
||||
{(profile?.starterPrompts?.length ?? 0) > 0 && (
|
||||
<ul className="mt-4 flex flex-wrap justify-center gap-2" aria-label="Suggested questions" data-qqq-id="assistant-starters">
|
||||
{profile?.starterPrompts?.map((prompt) => (
|
||||
<li key={prompt} className="max-w-full">
|
||||
<li key={prompt} className="w-full min-w-0 @md:w-auto @md:max-w-full" data-qqq-id="assistant-starter">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => send(prompt)}
|
||||
className="max-w-full rounded-full border border-border bg-background px-3 py-1.5 text-left text-xs text-foreground hover:bg-accent pointer-coarse:min-h-11 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60"
|
||||
className="w-full whitespace-normal break-words rounded-2xl border border-border bg-background px-3 py-1.5 text-left text-xs text-foreground hover:bg-accent pointer-coarse:min-h-11 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-60"
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
/** @file Exercises the real variant lifecycle with only process and filesystem boundaries replaced. */
|
||||
import { EventEmitter } from 'node:events'
|
||||
import path from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
@@ -39,7 +40,7 @@ vi.mock('node:child_process', () => {
|
||||
// Existing stale export and build files cannot determine the browser's selected server.
|
||||
vi.mock('node:fs', async (original) => {
|
||||
const fs = {
|
||||
...await original<typeof import('node:fs')>(), existsSync: (file: string) => file.endsWith('next-dashboard/index.html') ? state.hasExport : true, mkdirSync: () => {}, rmSync: () => {},
|
||||
...await original<typeof import('node:fs')>(), existsSync: (file: string) => file.endsWith(path.join('next-dashboard', 'index.html')) ? state.hasExport : true, mkdirSync: () => {}, rmSync: () => {},
|
||||
readdirSync: () => ['Fixture.java'],
|
||||
readFileSync: (file: string) => file.endsWith('routes-manifest.json')
|
||||
? JSON.stringify({ rewrites: { beforeFiles: [], afterFiles: ['qqq', 'data', 'widget', 'metaData', 'download', 'processes', 'possibleValues', 'reports', 'manageSession', 'apis.json', 'api', 'qa'].map((prefix) => ({ source: `/${prefix}/:path*`, destination: `http://127.0.0.1:${state.wrongTarget ? '18765' : '18775'}/${prefix}/:path*` })), fallback: [] } })
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// E2E: the assistant panel header (history / new / close buttons with tooltips) and the example
|
||||
// prompts: long prompts wrap inside their chips at every width, nothing clips, nothing scrolls
|
||||
// sideways. Set QQQ_ASSISTANT_SCREENSHOTS to a directory to keep a screenshot per viewport.
|
||||
|
||||
import { test, expect, type Page, type Route } from '@playwright/test'
|
||||
import { METADATA, setupApiMocks } from './api-mocks'
|
||||
import { checkA11y } from './a11y-helpers'
|
||||
|
||||
const VIEWPORTS = [
|
||||
{ name: '390', width: 390, height: 844 },
|
||||
{ name: '768', width: 768, height: 1024 },
|
||||
{ name: '1024', width: 1024, height: 768 },
|
||||
{ name: '1440', width: 1440, height: 900 },
|
||||
{ name: '1920', width: 1920, height: 1080 },
|
||||
{ name: '3200', width: 3200, height: 1800 },
|
||||
]
|
||||
|
||||
const SCREENSHOT_DIR = process.env.QQQ_ASSISTANT_SCREENSHOTS
|
||||
const CONVERSATION = { id: 4, title: 'Active people', status: 'ACTIVE', pinned: false, lastMessageAt: null, unreadCount: 0, createDate: '2026-10-12T00:00:00Z' }
|
||||
const PROMPTS = [
|
||||
'Which of my open orders have been waiting on a customer reply for more than two weeks, and what was the last thing we said to them?',
|
||||
'Summarize last quarter by region and flag anything that moved more than ten percent',
|
||||
'Show me every record I changed this week',
|
||||
]
|
||||
|
||||
/**
|
||||
* Mocks the assistant routes with three long example prompts and one saved conversation.
|
||||
*
|
||||
* @param page - The page.
|
||||
*/
|
||||
async function mockAssistant(page: Page) {
|
||||
const json = (route: Route, body: unknown, status = 200) => route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) })
|
||||
await page.route('**/qqq/v1/version', (route) => json(route, { apiVersion: '1.4.0', features: [], extensions: [{ name: 'assistant', version: '1.0.0' }] }))
|
||||
await page.route(/\/qqq\/v1\/metaData(\?|$)/, (route) => json(route, { ...METADATA, supplementalInstanceMetaData: { assistant: {} } }))
|
||||
await page.route('**/qqq/v1/assistant/**', (route) => {
|
||||
const path = new URL(route.request().url()).pathname.replace('/qqq/v1/assistant', '')
|
||||
if (path === '/config') return json(route, { enabled: true, shortcut: 'mod+j', model: 'claude-sonnet-5-5', features: { writes: false }, profile: { starterPrompts: PROMPTS } })
|
||||
if (path === '/conversations') return json(route, { conversations: [CONVERSATION] })
|
||||
return json(route, { error: 'not mocked' }, 404)
|
||||
})
|
||||
}
|
||||
|
||||
for (const viewport of VIEWPORTS) {
|
||||
test(`assistant panel chrome and example prompts at ${viewport.name}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height })
|
||||
await setupApiMocks(page)
|
||||
await mockAssistant(page)
|
||||
await page.goto('/app/person')
|
||||
await page.waitForSelector('[role="status"][aria-label="Loading"]', { state: 'hidden', timeout: 30000 }).catch(() => null)
|
||||
await expect(page.locator('[data-qqq-id="main-content"]')).toBeVisible({ timeout: 30000 })
|
||||
if (viewport.width < 640) {
|
||||
// compact header: the assistant opens from the account menu
|
||||
await page.locator('[data-qqq-id="button-account"]').click()
|
||||
await page.getByRole('menuitem', { name: 'Ask the assistant' }).click()
|
||||
} else {
|
||||
await page.locator('[data-qqq-id="button-assistant"]').click()
|
||||
}
|
||||
const panel = page.locator('[data-qqq-id="assistant-panel"]')
|
||||
await expect(panel).toBeVisible()
|
||||
const starters = panel.locator('[data-qqq-id="assistant-starters"]')
|
||||
await expect(starters.getByRole('button')).toHaveCount(3)
|
||||
|
||||
// nothing clipped: each chip's text box fits inside the chip, and the chips fit inside the list
|
||||
const clipped = await starters.evaluate((list) => {
|
||||
const bad: string[] = []
|
||||
const listBox = list.getBoundingClientRect()
|
||||
for (const chip of Array.from(list.querySelectorAll('button'))) {
|
||||
const box = chip.getBoundingClientRect()
|
||||
if (chip.scrollWidth > chip.clientWidth + 1 || chip.scrollHeight > chip.clientHeight + 1) bad.push('overflow')
|
||||
if (box.left < listBox.left - 1 || box.right > listBox.right + 1) bad.push('outside')
|
||||
}
|
||||
return bad
|
||||
})
|
||||
expect(clipped).toEqual([])
|
||||
expect(await page.evaluate(() => document.scrollingElement!.scrollWidth <= window.innerWidth + 1)).toBe(true)
|
||||
expect(await panel.locator('[data-qqq-id="assistant-messages"]').evaluate((el) => el.scrollWidth <= el.clientWidth + 1)).toBe(true)
|
||||
|
||||
if (SCREENSHOT_DIR) await page.screenshot({ path: `${SCREENSHOT_DIR}/empty-${viewport.name}.png` })
|
||||
|
||||
// history rail open
|
||||
await panel.locator('[data-qqq-id="assistant-rail-toggle"]').click()
|
||||
await expect(panel.getByRole('button', { name: 'Active people', exact: true })).toBeVisible()
|
||||
await panel.locator('[data-qqq-id="assistant-rail-toggle"]').focus()
|
||||
await page.waitForTimeout(600)
|
||||
if (SCREENSHOT_DIR) await page.screenshot({ path: `${SCREENSHOT_DIR}/history-${viewport.name}.png` })
|
||||
await checkA11y(page)
|
||||
})
|
||||
}
|
||||
@@ -49,10 +49,13 @@ it('keeps the variant build, static assets and entrypoint after the main build d
|
||||
expect(readFileSync(path.join(staged, 'public/icon.svg'), 'utf8')).toBe('<svg/>')
|
||||
expect(readFileSync(path.join(staged, 'qqq-server.mjs'), 'utf8')).toBe('container entrypoint')
|
||||
expect(readFileSync(path.join(staged, 'branding-assets.mjs'), 'utf8')).toBe('declared branding aliases')
|
||||
expect(readlinkSync(path.join(staged, 'node_modules/example'))).toBe('.pnpm/example')
|
||||
expect(readlinkSync(path.join(staged, 'node_modules/example'))).toBe(path.join('.pnpm', 'example'))
|
||||
expect(readFileSync(path.join(staged, 'node_modules/example/index.js'), 'utf8')).toBe('independent dependency')
|
||||
expect(statSync(path.join(staged, 'server.js')).mode & 0o777).toBe(0o600)
|
||||
expect(statSync(path.join(staged, '.next/static')).mode & 0o777).toBe(0o700)
|
||||
// Windows does not implement POSIX owner/group/other permission bits.
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(path.join(staged, 'server.js')).mode & 0o777).toBe(0o600)
|
||||
expect(statSync(path.join(staged, '.next/static')).mode & 0o777).toBe(0o700)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an old shared-backend rewrite even with a new valid variant marker', () => {
|
||||
|
||||
Reference in new issue
Block a user