('vue-i18n')
+ return {
+ ...actual,
+ useI18n: () => ({ t: (key: string) => key }),
+ }
+})
const componentPath = resolve(dirname(fileURLToPath(import.meta.url)), '../AvailableChannelsTable.vue')
const componentSource = readFileSync(componentPath, 'utf8')
@@ -18,3 +30,150 @@ describe('AvailableChannelsTable scroll integration', () => {
expect(componentSource).not.toMatch(//)
})
})
+
+const rows: UserAvailableChannel[] = [
+ {
+ name: 'Primary channel',
+ description: 'Fast and reliable access',
+ platforms: [
+ {
+ platform: 'anthropic',
+ groups: [
+ {
+ id: 1,
+ name: 'Exclusive Pro',
+ platform: 'anthropic',
+ subscription_type: 'standard',
+ rate_multiplier: 1.2,
+ peak_rate_enabled: true,
+ peak_start: '08:00',
+ peak_end: '10:00',
+ peak_rate_multiplier: 1.5,
+ is_exclusive: true,
+ },
+ {
+ id: 2,
+ name: 'Public',
+ platform: 'anthropic',
+ subscription_type: 'standard',
+ rate_multiplier: 1,
+ peak_rate_enabled: false,
+ peak_start: '',
+ peak_end: '',
+ peak_rate_multiplier: 1,
+ is_exclusive: false,
+ },
+ ],
+ supported_models: [{ name: 'claude-test', platform: 'anthropic', pricing: null }],
+ },
+ ],
+ },
+]
+
+const baseProps = {
+ columns: {
+ name: 'Channel',
+ description: 'Description',
+ platform: 'Platform',
+ groups: 'Groups and rates',
+ supportedModels: 'Models and pricing',
+ },
+ rows,
+ loading: false,
+ pricingKeyPrefix: 'availableChannels.pricing',
+ noPricingLabel: 'No pricing',
+ noModelsLabel: 'No models',
+ emptyLabel: 'No channels',
+ userGroupRates: { 1: 0.8 },
+}
+
+function mountTable(props = {}) {
+ return mount(AvailableChannelsTable, {
+ props: { ...baseProps, ...props },
+ global: {
+ plugins: [createPinia()],
+ stubs: {
+ Icon: { props: ['name'], template: '' },
+ PlatformIcon: { template: '' },
+ GroupBadge: {
+ props: ['name', 'rateMultiplier', 'userRateMultiplier'],
+ template:
+ '{{ name }}:{{ rateMultiplier }}:{{ userRateMultiplier }}',
+ },
+ SupportedModelChip: {
+ props: ['model', 'noPricingLabel'],
+ template: '{{ model.name }}:{{ noPricingLabel }}',
+ },
+ },
+ },
+ })
+}
+
+describe('AvailableChannelsTable responsive surfaces', () => {
+ it('keeps the five-column table as the desktop-only surface', () => {
+ const wrapper = mountTable()
+ const desktop = wrapper.get('[data-testid="desktop-channels"]')
+
+ // TablePageLayout has a more-specific `display: table` rule for descendants,
+ // so these display utilities must remain important at both breakpoints.
+ expect(desktop.classes()).toContain('!hidden')
+ expect(desktop.classes()).toContain('lg:!table')
+ expect(desktop.findAll('thead th')).toHaveLength(5)
+ expect(desktop.text()).toContain('Primary channel')
+ expect(desktop.text()).toContain('Fast and reliable access')
+ expect(desktop.findAll('[data-group-badge]')).toHaveLength(2)
+ expect(desktop.get('[data-model-chip]').text()).toContain('claude-test:No pricing')
+ })
+
+ it('renders a mobile-only readable surface with groups, rates, peaks, and model pricing chips', () => {
+ const wrapper = mountTable()
+ const mobile = wrapper.get('[data-testid="mobile-channels"]')
+
+ expect(mobile.classes()).toContain('lg:hidden')
+ expect(mobile.classes()).toContain('overflow-x-hidden')
+ expect(mobile.text()).toContain('Primary channel')
+ expect(mobile.text()).toContain('Fast and reliable access')
+ expect(mobile.text()).toContain('Groups and rates')
+ expect(mobile.text()).toContain('Models and pricing')
+ expect(mobile.text()).toContain('availableChannels.exclusive')
+ expect(mobile.text()).toContain('availableChannels.public')
+ expect(mobile.get('[data-group-badge]').text()).toBe('Exclusive Pro:1.2:0.8')
+ expect(mobile.findAll('[data-group-badge]')).toHaveLength(2)
+ expect(mobile.get('[data-icon="clock"]')).toBeTruthy()
+ expect(mobile.text()).toContain('08:00')
+ expect(mobile.text()).toContain('10:00')
+ expect(mobile.text()).toContain('×1.5')
+ expect(mobile.get('[data-model-chip]').text()).toBe('claude-test:No pricing')
+ expect(mobile.findAll('.max-w-full')).not.toHaveLength(0)
+ })
+
+ it('keeps the mobile placeholders when a platform has no groups or models', () => {
+ const wrapper = mountTable({
+ rows: [
+ {
+ name: 'Fallback channel',
+ description: '',
+ platforms: [{ platform: 'openai', groups: [], supported_models: [] }],
+ },
+ ],
+ })
+ const mobile = wrapper.get('[data-testid="mobile-channels"]')
+
+ expect(mobile.text()).toContain('Fallback channel')
+ expect(mobile.text()).toContain('openai')
+ expect(mobile.text()).toContain('No models')
+ expect(mobile.findAll('dd')[0].text()).toBe('-')
+ })
+
+ it('provides loading and empty states on both responsive surfaces', async () => {
+ const wrapper = mountTable({ loading: true, rows: [] })
+
+ expect(wrapper.get('[data-testid="desktop-channels"] [data-icon="refresh"]')).toBeTruthy()
+ expect(wrapper.get('[data-testid="mobile-loading"] [data-icon="refresh"]')).toBeTruthy()
+
+ await wrapper.setProps({ loading: false })
+
+ expect(wrapper.get('[data-testid="desktop-channels"]').text()).toContain('No channels')
+ expect(wrapper.get('[data-testid="mobile-empty"]').text()).toContain('No channels')
+ })
+})