mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
模型广场:按计费阶梯单价表展示长上下文档位
- 新建 ModelPlazaService(持计费服务与定价解析器)承接广场聚合, token 模型的单价与档位全部取自 ResolveContextPricingSchedule, 渠道选择与计费同源;图片/按次模型沿用原档位合成 - 官方参考价改走计费目录(LiteLLM → 内置兜底 → 模型策略),带官方阶梯 - DTO 增加 long_context_pricing_enabled / long_context_basis / official_pricing.intervals - 前端实付与官方三列按档分行(标签只在首列,其余列按行对齐), 缓存列按档展示写/读价,边际计价以徽章与 tooltip 标注, 分组关闭阶梯时在头部说明
This commit is contained in:
@@ -42,6 +42,13 @@
|
||||
<Icon name="clock" size="xs" class="h-3 w-3" />
|
||||
{{ peakNote }}
|
||||
</p>
|
||||
<p
|
||||
v-if="longContextNote"
|
||||
class="mt-1.5 flex items-center gap-1 text-xs text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<Icon name="infoCircle" size="xs" class="h-3 w-3" />
|
||||
{{ longContextNote }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 模型价格表:整行(含 hover 底色/分区底色)顶到卡片边缘,左右留白由表格首列/末列的 padding 提供 -->
|
||||
@@ -92,4 +99,16 @@ const peakNote = computed(() => {
|
||||
multiplier: props.group.peak_rate_multiplier
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 分组关闭了长上下文阶梯、但组内有模型官方带阶梯时提示:实付列只展示基础档,
|
||||
* 官方阶梯仅供参考。字段缺失(旧后端)不提示。
|
||||
*/
|
||||
const longContextNote = computed(() => {
|
||||
if (props.group.long_context_pricing_enabled !== false) return ''
|
||||
const hasOfficialLadder = props.group.models.some(
|
||||
(m) => (m.official_pricing?.intervals?.length ?? 0) > 1
|
||||
)
|
||||
return hasOfficialLadder ? t('modelPlaza.detail.longContextDisabledNote') : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="plaza-pricing-table overflow-x-auto" :style="accentStyle">
|
||||
<table class="w-full min-w-[860px] table-fixed border-collapse text-sm tabular-nums">
|
||||
<table class="w-full min-w-[1000px] table-fixed border-collapse text-sm tabular-nums">
|
||||
<colgroup>
|
||||
<col class="w-[22%]" />
|
||||
<col class="w-[10%]" />
|
||||
<col class="w-[10%]" />
|
||||
<col class="w-[14%]" />
|
||||
<col class="w-[10%]" />
|
||||
<col class="w-[10%]" />
|
||||
<col class="w-[14%]" />
|
||||
<col class="w-[20%]" />
|
||||
<col class="w-[11%]" />
|
||||
<col class="w-[9%]" />
|
||||
<col class="w-[15%]" />
|
||||
<col class="w-[11%]" />
|
||||
<col class="w-[9%]" />
|
||||
<col class="w-[15%]" />
|
||||
<col class="w-[10%]" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
@@ -81,10 +81,17 @@
|
||||
>
|
||||
{{ billingModeLabel(m) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="m.long_context_basis === 'marginal'"
|
||||
class="rounded-md bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:bg-dark-700/70 dark:text-dark-300"
|
||||
:title="t('modelPlaza.table.tierHintMarginal')"
|
||||
>
|
||||
{{ t('modelPlaza.table.marginalBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- token 计费:输入 / 输出(阶梯内联)/ 缓存(写/读) -->
|
||||
<!-- token 计费:输入 / 输出 / 缓存(写/读),有阶梯时每档一行;档位标签只放输入列,其余列按行对齐 -->
|
||||
<template v-if="billingMode(m) === BILLING_MODE_TOKEN">
|
||||
<td class="pz-cell px-3 py-2.5 align-middle font-mono font-semibold text-gray-900 dark:text-gray-50">
|
||||
<template v-if="tokenIntervals(m).length">
|
||||
@@ -93,7 +100,7 @@
|
||||
:key="idx"
|
||||
class="whitespace-nowrap text-xs leading-5"
|
||||
>
|
||||
<span class="mr-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ tierLabel(iv) }}</span>
|
||||
<span class="mr-1 font-sans font-normal text-gray-400 dark:text-dark-500" :title="tierHint(m)">{{ tierLabel(iv) }}</span>
|
||||
{{ paidPerMillion(iv.input_price) }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,16 +112,32 @@
|
||||
v-for="(iv, idx) in tokenIntervals(m)"
|
||||
:key="idx"
|
||||
class="whitespace-nowrap text-xs leading-5"
|
||||
:title="tierHint(m)"
|
||||
>
|
||||
<span class="mr-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ tierLabel(iv) }}</span>
|
||||
{{ paidPerMillion(iv.output_price) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>{{ paidPerMillion(m.pricing?.output_price) }}</template>
|
||||
</td>
|
||||
<td class="pz-cell px-3 py-2.5 align-middle">
|
||||
<template v-if="hasTierCachePricing(tokenIntervals(m))">
|
||||
<div
|
||||
v-for="(iv, idx) in tokenIntervals(m)"
|
||||
:key="idx"
|
||||
class="whitespace-nowrap font-mono text-xs leading-5 text-gray-800 dark:text-gray-200"
|
||||
:title="tierHint(m)"
|
||||
>
|
||||
<template v-if="iv.cache_write_price != null || iv.cache_read_price != null">
|
||||
<span class="font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheWriteShort') }}</span>
|
||||
{{ paidPerMillion(iv.cache_write_price) }}
|
||||
<span class="ml-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheReadShort') }}</span>
|
||||
{{ paidPerMillion(iv.cache_read_price) }}
|
||||
</template>
|
||||
<span v-else class="text-gray-400 dark:text-dark-500">-</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="hasCachePricing(m)"
|
||||
v-else-if="hasCachePricing(m)"
|
||||
class="space-y-0.5 font-mono text-xs text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<div>
|
||||
@@ -157,18 +180,54 @@
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<!-- 官方价格(LiteLLM 参考价,不乘倍率) -->
|
||||
<!-- 官方价格(参考价,不乘倍率;官方有阶梯时每档一行) -->
|
||||
<td
|
||||
class="border-l border-gray-100 px-3 py-2.5 align-middle font-mono text-xs text-gray-500 dark:border-dark-700/60 dark:text-dark-400"
|
||||
>
|
||||
{{ official(m.official_pricing?.input_price) }}
|
||||
<template v-if="officialIntervals(m).length">
|
||||
<div
|
||||
v-for="(iv, idx) in officialIntervals(m)"
|
||||
:key="idx"
|
||||
class="whitespace-nowrap leading-5"
|
||||
>
|
||||
<span class="mr-1 font-sans text-gray-400 dark:text-dark-500" :title="t('modelPlaza.table.tierHint')">{{ tierLabel(iv) }}</span>
|
||||
{{ official(iv.input_price) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>{{ official(m.official_pricing?.input_price) }}</template>
|
||||
</td>
|
||||
<td class="px-3 py-2.5 align-middle font-mono text-xs text-gray-500 dark:text-dark-400">
|
||||
{{ official(m.official_pricing?.output_price) }}
|
||||
<template v-if="officialIntervals(m).length">
|
||||
<div
|
||||
v-for="(iv, idx) in officialIntervals(m)"
|
||||
:key="idx"
|
||||
class="whitespace-nowrap leading-5"
|
||||
:title="t('modelPlaza.table.tierHint')"
|
||||
>
|
||||
{{ official(iv.output_price) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>{{ official(m.official_pricing?.output_price) }}</template>
|
||||
</td>
|
||||
<td class="px-3 py-2.5 align-middle">
|
||||
<template v-if="hasTierCachePricing(officialIntervals(m))">
|
||||
<div
|
||||
v-for="(iv, idx) in officialIntervals(m)"
|
||||
:key="idx"
|
||||
class="whitespace-nowrap font-mono text-xs leading-5 text-gray-500 dark:text-dark-400"
|
||||
:title="t('modelPlaza.table.tierHint')"
|
||||
>
|
||||
<template v-if="iv.cache_write_price != null || iv.cache_read_price != null">
|
||||
<span class="font-sans text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheWriteShort') }}</span>
|
||||
{{ official(iv.cache_write_price) }}
|
||||
<span class="ml-1 font-sans text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheReadShort') }}</span>
|
||||
{{ official(iv.cache_read_price) }}
|
||||
</template>
|
||||
<span v-else class="text-gray-400 dark:text-dark-500">-</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="m.official_pricing && hasOfficialCache(m.official_pricing)"
|
||||
v-else-if="m.official_pricing && hasOfficialCache(m.official_pricing)"
|
||||
class="space-y-0.5 font-mono text-xs text-gray-500 dark:text-dark-400"
|
||||
>
|
||||
<div>
|
||||
@@ -322,23 +381,46 @@ function hasOfficialCache(o: NonNullable<PlazaModel['official_pricing']>): boole
|
||||
return o.cache_write_price != null || o.cache_read_price != null || o.cache_write_1h_price != null
|
||||
}
|
||||
|
||||
/** token 模式的阶梯定价(内联进输入/输出列)。 */
|
||||
/** token 模式的阶梯定价(内联进输入/输出/缓存列)。 */
|
||||
function tokenIntervals(m: PlazaModel): UserPricingInterval[] {
|
||||
return m.pricing?.intervals ?? []
|
||||
}
|
||||
|
||||
/** 官方阶梯(后端按目录规则合成,不受分组开关影响)。 */
|
||||
function officialIntervals(m: PlazaModel): UserPricingInterval[] {
|
||||
return m.official_pricing?.intervals ?? []
|
||||
}
|
||||
|
||||
/** 任一档带缓存价才按档渲染缓存列;否则沿用平价的写入/读取两行。 */
|
||||
function hasTierCachePricing(intervals: UserPricingInterval[]): boolean {
|
||||
return intervals.some((iv) => iv.cache_write_price != null || iv.cache_read_price != null)
|
||||
}
|
||||
|
||||
/** 档位说明:整单按档计价,或(平台旧规则)仅超出部分按档计价。 */
|
||||
function tierHint(m: PlazaModel): string {
|
||||
return m.long_context_basis === 'marginal'
|
||||
? t('modelPlaza.table.tierHintMarginal')
|
||||
: t('modelPlaza.table.tierHint')
|
||||
}
|
||||
|
||||
|
||||
/** 按次/按图模式的阶梯定价(仅保留配了按次价的档位)。 */
|
||||
function requestIntervals(m: PlazaModel): UserPricingInterval[] {
|
||||
return (m.pricing?.intervals ?? []).filter((iv) => iv.per_request_price != null)
|
||||
}
|
||||
|
||||
/** 档位标签:优先管理员配置的 tier_label,否则按 token 区间生成(≤200K / >200K / 200K–1M)。 */
|
||||
/** 档位标签:优先管理员配置的 tier_label,否则按 token 区间生成(≤200K / >200K / 100–200K / 200K–1M)。 */
|
||||
function tierLabel(iv: UserPricingInterval): string {
|
||||
if (iv.tier_label) return iv.tier_label
|
||||
const { min_tokens: min, max_tokens: max } = iv
|
||||
if (max == null) return `>${formatTokenCount(min)}`
|
||||
if (min === 0) return `≤${formatTokenCount(max)}`
|
||||
return `${formatTokenCount(min)}–${formatTokenCount(max)}`
|
||||
const lo = formatTokenCount(min)
|
||||
const hi = formatTokenCount(max)
|
||||
// 同单位时省略前一个单位(100–200K),节省列宽
|
||||
const unit = hi.slice(-1)
|
||||
if (/[KM]/.test(unit) && lo.endsWith(unit)) return `${lo.slice(0, -1)}–${hi}`
|
||||
return `${lo}–${hi}`
|
||||
}
|
||||
|
||||
function formatTokenCount(n: number): string {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PlazaGroupSection from '../PlazaGroupSection.vue'
|
||||
import type { ModelPlazaGroup, PlazaModel } from '@/api/modelPlaza'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ cachedPublicSettings: null })
|
||||
}))
|
||||
|
||||
function ladderModel(tiers: number): PlazaModel {
|
||||
const intervals = Array.from({ length: tiers }, (_, i) => ({
|
||||
min_tokens: i * 272000,
|
||||
max_tokens: i === tiers - 1 ? null : (i + 1) * 272000,
|
||||
tier_label: '',
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
per_request_price: null
|
||||
}))
|
||||
return {
|
||||
name: 'gpt-5.6-sol',
|
||||
platform: 'openai',
|
||||
pricing: {
|
||||
billing_mode: 'token',
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
image_input_price: null,
|
||||
image_output_price: null,
|
||||
per_request_price: null,
|
||||
intervals: []
|
||||
},
|
||||
official_pricing: {
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: null,
|
||||
cache_read_price: null,
|
||||
intervals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function group(overrides: Partial<ModelPlazaGroup> = {}): ModelPlazaGroup {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'g',
|
||||
description: '',
|
||||
platform: 'openai',
|
||||
subscription_type: 'standard',
|
||||
rate_multiplier: 1,
|
||||
peak_rate_enabled: false,
|
||||
peak_start: '',
|
||||
peak_end: '',
|
||||
peak_rate_multiplier: 1,
|
||||
is_exclusive: false,
|
||||
image_rate_independent: false,
|
||||
image_rate_multiplier: 1,
|
||||
long_context_pricing_enabled: true,
|
||||
models: [ladderModel(2)],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function mountSection(g: ModelPlazaGroup) {
|
||||
return mount(PlazaGroupSection, {
|
||||
props: { group: g },
|
||||
global: {
|
||||
stubs: {
|
||||
GroupBadge: true,
|
||||
Icon: true,
|
||||
PlazaModelPricingTable: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const NOTE = 'modelPlaza.detail.longContextDisabledNote'
|
||||
|
||||
describe('PlazaGroupSection 长上下文说明', () => {
|
||||
it('分组关闭阶梯且组内有官方阶梯模型时显示说明', () => {
|
||||
const wrapper = mountSection(group({ long_context_pricing_enabled: false }))
|
||||
expect(wrapper.text()).toContain(NOTE)
|
||||
})
|
||||
|
||||
it('分组开启阶梯时不显示', () => {
|
||||
const wrapper = mountSection(group({ long_context_pricing_enabled: true }))
|
||||
expect(wrapper.text()).not.toContain(NOTE)
|
||||
})
|
||||
|
||||
it('分组关闭但没有官方阶梯模型时不显示', () => {
|
||||
const wrapper = mountSection(
|
||||
group({ long_context_pricing_enabled: false, models: [ladderModel(1)] })
|
||||
)
|
||||
expect(wrapper.text()).not.toContain(NOTE)
|
||||
})
|
||||
|
||||
it('旧后端缺少开关字段时不显示', () => {
|
||||
const g = group()
|
||||
delete (g as Partial<ModelPlazaGroup>).long_context_pricing_enabled
|
||||
const wrapper = mountSection(g)
|
||||
expect(wrapper.text()).not.toContain(NOTE)
|
||||
})
|
||||
})
|
||||
@@ -405,3 +405,133 @@ describe('PlazaModelPricingTable', () => {
|
||||
expect(wrapper.text()).toContain('OpenAI')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PlazaModelPricingTable 长上下文阶梯', () => {
|
||||
function ladderIntervals() {
|
||||
return [
|
||||
{
|
||||
min_tokens: 0,
|
||||
max_tokens: 272000,
|
||||
tier_label: '≤272K',
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: 6.25e-6,
|
||||
cache_read_price: 5e-7,
|
||||
per_request_price: null
|
||||
},
|
||||
{
|
||||
min_tokens: 272000,
|
||||
max_tokens: null,
|
||||
tier_label: '>272K',
|
||||
input_price: 1e-5,
|
||||
output_price: 4.5e-5,
|
||||
cache_write_price: 1.25e-5,
|
||||
cache_read_price: 1e-6,
|
||||
per_request_price: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function ladderModel(overrides: Partial<PlazaModel> = {}): PlazaModel {
|
||||
return tokenModel({
|
||||
name: 'gpt-5.6-sol',
|
||||
platform: 'openai',
|
||||
pricing: {
|
||||
billing_mode: 'token',
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: 6.25e-6,
|
||||
cache_read_price: 5e-7,
|
||||
image_input_price: null,
|
||||
image_output_price: null,
|
||||
per_request_price: null,
|
||||
intervals: ladderIntervals()
|
||||
},
|
||||
official_pricing: {
|
||||
input_price: 5e-6,
|
||||
output_price: 3e-5,
|
||||
cache_write_price: 6.25e-6,
|
||||
cache_read_price: 5e-7,
|
||||
intervals: ladderIntervals()
|
||||
},
|
||||
long_context_basis: 'whole_request',
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
it('实付缓存列按档分行并乘倍率,每档一行与输入/输出列对齐;档位标签只在输入列', () => {
|
||||
const wrapper = mountTable([ladderModel()], 0.5)
|
||||
const cells = wrapper.findAll('tbody td')
|
||||
const cacheCell = cells[3]
|
||||
const rows = cacheCell.findAll('.leading-5')
|
||||
expect(rows).toHaveLength(2)
|
||||
// 写 6.25 × 0.5 / 读 0.5 × 0.5;高档 12.5 × 0.5 / 1 × 0.5
|
||||
expect(rows[0].text()).toContain('modelPlaza.table.cacheWriteShort')
|
||||
expect(rows[0].text()).toContain('$3.125')
|
||||
expect(rows[0].text()).toContain('$0.25')
|
||||
expect(rows[1].text()).toContain('$6.25')
|
||||
expect(rows[1].text()).toContain('$0.50')
|
||||
// 输入列带标签,输出/缓存列只按行对齐不重复标签
|
||||
expect(cells[1].text()).toContain('≤272K')
|
||||
expect(cells[1].text()).toContain('>272K')
|
||||
expect(cells[2].text()).not.toContain('272K')
|
||||
expect(cacheCell.text()).not.toContain('272K')
|
||||
expect(cells[1].findAll('.leading-5')).toHaveLength(2)
|
||||
expect(cells[2].findAll('.leading-5')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('官方三列按 official_pricing.intervals 分档且不乘倍率,不内联 1h', () => {
|
||||
const wrapper = mountTable([ladderModel()], 0.5)
|
||||
const cells = wrapper.findAll('tbody td')
|
||||
expect(cells[4].text()).toContain('≤272K')
|
||||
expect(cells[4].text()).toContain('$5.00')
|
||||
expect(cells[4].text()).toContain('>272K')
|
||||
expect(cells[4].text()).toContain('$10.00')
|
||||
expect(cells[5].text()).toContain('$30.00')
|
||||
expect(cells[5].text()).toContain('$45.00')
|
||||
expect(cells[6].text()).toContain('$6.25')
|
||||
expect(cells[6].text()).toContain('$12.50')
|
||||
expect(cells[6].text()).toContain('$1.00')
|
||||
expect(cells[6].text()).not.toContain('(1h')
|
||||
})
|
||||
|
||||
it('整单计价的档位标签带 tooltip;边际计价在模型名旁加徽章并换用边际说明', () => {
|
||||
const whole = mountTable([ladderModel()], 1)
|
||||
const wholeLabels = whole.findAll('tbody td span[title="modelPlaza.table.tierHint"]')
|
||||
expect(wholeLabels.length).toBeGreaterThan(0)
|
||||
expect(whole.text()).not.toContain('modelPlaza.table.marginalBadge')
|
||||
|
||||
const marginal = mountTable([ladderModel({ long_context_basis: 'marginal' })], 1)
|
||||
const marginalLabels = marginal.findAll('tbody td span[title="modelPlaza.table.tierHintMarginal"]')
|
||||
expect(marginalLabels.length).toBeGreaterThan(0)
|
||||
expect(marginal.findAll('tbody td')[0].text()).toContain('modelPlaza.table.marginalBadge')
|
||||
})
|
||||
|
||||
it('自定义中间档标签同单位时省略前一个单位', () => {
|
||||
const model = ladderModel({
|
||||
pricing: {
|
||||
...ladderModel().pricing!,
|
||||
intervals: [
|
||||
{ ...ladderIntervals()[0], max_tokens: 100000, tier_label: '' },
|
||||
{ ...ladderIntervals()[0], min_tokens: 100000, max_tokens: 200000, tier_label: '' },
|
||||
{ ...ladderIntervals()[1], min_tokens: 200000, max_tokens: 1000000, tier_label: '' },
|
||||
{ ...ladderIntervals()[1], min_tokens: 1000000, tier_label: '' }
|
||||
]
|
||||
}
|
||||
})
|
||||
const text = mountTable([model], 1).findAll('tbody td')[1].text()
|
||||
expect(text).toContain('≤100K')
|
||||
expect(text).toContain('100–200K')
|
||||
expect(text).toContain('200K–1M')
|
||||
expect(text).toContain('>1M')
|
||||
})
|
||||
|
||||
it('官方无 intervals 字段(旧响应)时官方列保持平价,实付无阶梯时缓存列保持两行', () => {
|
||||
const wrapper = mountTable([tokenModel()], 1)
|
||||
const cells = wrapper.findAll('tbody td')
|
||||
expect(cells[3].text()).toContain('modelPlaza.table.cacheWrite')
|
||||
expect(cells[3].text()).toContain('modelPlaza.table.cacheRead')
|
||||
expect(cells[3].findAll('.leading-5')).toHaveLength(0)
|
||||
expect(cells[6].text()).toContain('(1h')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user