Skip to content
Merged
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
11 changes: 7 additions & 4 deletions apps/web/e2e/category-spec-template.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ test('earbud page shows audio specs and no phone fields', async ({ page }) => {
await page.goto(BUD);
const specs = page.locator('#specs-section');
const headings = specs.locator('h3');
await expect(headings.first()).toHaveText(/^1\. Âm thanh/);
// One language and no number (DOS/Rate#1529).
await expect(headings.first()).toHaveText(/^Âm thanh/);
// Groups with no value are hidden (DOS/Rate#1186) and no sale status is
// invented (DOS/Rate#1193): the seeded bud has audio, battery and
// Bluetooth, nothing else.
await expect(headings).toHaveCount(3);
await expect(headings.nth(1)).toHaveText(/^2\. Pin/);
await expect(headings.nth(1)).toHaveText(/^Pin/);
await expect(specs).toContainText('E2E 10.5mm driver');
await expect(specs).toContainText('E2E ANC');
for (const text of [
Expand Down Expand Up @@ -62,11 +63,13 @@ test('a watch page never shows 5G', async ({ page }) => {
await expect(meta).toContainText('Apple');
await expect(meta).not.toContainText('5G');
const specs = page.locator('#specs-section');
await expect(specs).toContainText('Màn hình (Display)');
await expect(specs).toContainText('Màn hình');
// One language per page (DOS/Rate#1529).
await expect(specs).not.toContainText('(Display)');
// The seeded watch has no network value left once 5G is dropped, so the
// cellular group is hidden (DOS/Rate#1186); the 4G-only bands label is
// covered by product-spec-rows.test.ts.
await expect(specs).not.toContainText('Mạng Di động');
await expect(specs).not.toContainText('Mạng di động');
await expect(specs).not.toContainText('5G');
await expect(specs).not.toContainText('Camera');
});
3 changes: 2 additions & 1 deletion apps/web/e2e/dark-hero-contrast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ for (const path of PAGES) {
}
const bg = Math.max(...stops);
const texts = surface.querySelectorAll<HTMLElement>(
'[class*="text-muted"], [class*="text-secondary"], [class*="text-success"], [class*="text-danger"], [class*="text-info"], [class*="text-warning"], [class*="text-primary"]'
// The star ratings' value and count too (DOS/Rate#1528).
'[class*="text-muted"], [class*="text-secondary"], [class*="text-success"], [class*="text-danger"], [class*="text-info"], [class*="text-warning"], [class*="text-primary"], .rate-stars-value, .rate-stars-count, .rate-stars.is-empty'
);
for (const el of texts) {
// Measured against its own nearest dark surface, once.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/inspect-copy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ for (const [prefix, lang] of [
`${prefix}/inspect?q=${encodeURIComponent('iPhone 16 Pro Max')}`
);
await expect(page.locator('html')).toHaveAttribute('lang', lang);
await expect(page.locator('.rate-score-badge-user').first()).toBeVisible();
await expect(page.locator('.rate-stars').first()).toBeVisible();
await page.locator('.rate-export-menu summary').click();
await page.getByTestId('inspect-copy-md-btn').click();
const labels = inspectLabels(lang);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/item-model/item-product-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const ITEM = '/smartphones/e2e-item-phone';
const BIG = '/smartphones/e2e-item-big';
const NO_DATA_MERCHANT = '17e00000-0000-4000-8000-0000000d1484';
const ORDER = [
'offers-table',
'pdv-price-history',
'specs-section',
'jreviews-section',
'pdv-price-history',
'offers-table',
];

/** Clicks once the page is hydrated: a click before it does nothing. */
Expand Down Expand Up @@ -72,7 +72,7 @@ test('the size picker replaces the quick specs and links each size', async ({

// The lazy Item also with another size loaded on demand.
for (const path of [ITEM, BIG, `${BIG}?v=256gb`]) {
test(`${path}: price, jump row, sellers, history, specs, reviews`, async ({
test(`${path}: price, jump row, specs, reviews, history, sellers`, async ({
page,
}) => {
await page.goto(path);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/e2e/product-above-fold.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
// On a phone the product name, the price line and the user score are in the
// first screen, not behind a full-screen photo (DOS/Rate#605). The score value
// itself, not only its label, must fit (DOS/Rate#630). So does the whole jump row to
// the sellers, history, specs and reviews (DOS/Rate#1517).
// the specs, reviews, history and sellers (DOS/Rate#1517, #1530).
const PRODUCT = '/smartphones/iphone-16-pro-max-256gb';

for (const viewport of [
Expand Down Expand Up @@ -36,6 +36,6 @@ for (const viewport of [
test('the repeated hero score is hidden on desktop', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto(PRODUCT);
await expect(page.locator('.rate-pdv-score-value').first()).toBeVisible();
await expect(page.getByTestId('pdv-user-score')).toBeVisible();
await expect(page.getByTestId('pdv-hero-score')).toBeHidden();
});
12 changes: 6 additions & 6 deletions apps/web/e2e/product-section-order.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { expect, test } from '@playwright/test';

// A shopper reaches the sellers first: the page reads price box, sellers,
// price history, specs, then reviews, with a jump row to each, and phones
// show the first three reviews until expanded (DOS/Rate#1469).
// The product comes first: the page reads price box, specs, reviews, price
// history, then the sellers last, with a jump row to each (DOS/Rate#1469,
// #1530), and phones show the first three reviews until expanded.
const PRODUCT = '/smartphones/iphone-16-pro-max-256gb';
// Six seeded reviews (seed.sql, DOS/Rate#566).
const MANY_REVIEWS = '/smartphones/e2e-review-labels-phone';
const ORDER = [
'offers-table',
'pdv-price-history',
'specs-section',
'jreviews-section',
'pdv-price-history',
'offers-table',
];

test('sellers come first, then price history, specs and reviews', async ({
test('specs come first, then reviews, price history and sellers', async ({
page,
}) => {
await page.goto(PRODUCT);
Expand Down
57 changes: 57 additions & 0 deletions apps/web/e2e/star-rating.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from '@playwright/test';

// Ratings show as stars with the value and a short count, with no verbose
// words around them (DOS/Rate#1528).
const PRODUCT = '/smartphones/iphone-16-pro-max-256gb';

test('the product scoreboard shows the buyer score as stars with a count', async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 900 });
expect((await page.goto(PRODUCT))?.status()).toBe(200);

const score = page.getByTestId('pdv-user-score');
await expect(score).toBeVisible();
await expect(score).toHaveAttribute('role', 'img');
// "4.6 trên 5 sao, 6 đánh giá": one label for screen readers.
await expect(score).toHaveAttribute(
'aria-label',
/^\d\.\d trên 5 sao, [\d.]+ đánh giá$/
);
await expect(score.locator('.rate-stars-value')).toHaveText(/^\d\.\d$/);
await expect(score.locator('.rate-stars-count')).toHaveText(/^\([\d.]+\)$/);
// The band colour follows the printed value.
const value = Number(await score.getAttribute('data-rating'));
const band = value >= 4 ? 'is-good' : value >= 3 ? 'is-fair' : 'is-poor';
await expect(score).toHaveClass(new RegExp(`\\b${band}\\b`));
// The fill is the share of five stars.
const fill = await score
.locator('.rate-stars-fill')
.evaluate((el) => (el as HTMLElement).style.width);
expect(Number.parseFloat(fill)).toBeCloseTo((value / 5) * 100, 5);

const text = await page.locator('main, body').first().innerText();
for (const phrase of [
'bài kiểm chứng',
'chuẩn 10 nhóm',
'(có trọng số)',
'/ 5.0',
'(Bayes)',
]) {
expect(text, phrase).not.toContain(phrase);
}
});

test('product cards and the leaderboard use the same stars', async ({
page,
}) => {
expect((await page.goto('/smartphones'))?.status()).toBe(200);
await expect(
page.locator('.rate-product-card .rate-stars').first()
).toBeVisible();
expect((await page.goto('/leaderboard'))?.status()).toBe(200);
await expect(page.locator('.rate-stars[role="img"]').first()).toBeVisible();
const text = await page.locator('body').innerText();
expect(text).not.toContain('/5.0');
expect(text).not.toContain('/ 5.0');
});
35 changes: 17 additions & 18 deletions apps/web/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@
"versusMetaDescription": "Přímé porovnání {nameA} a {nameB}: hodnocení v 5 kritériích, 10 skupin parametrů a skutečné ceny v obchodech.",
"versusHeroBadge": "Souboj 1v1 • stejná kategorie ({category})",
"viewCategoryLeaderboard": "Zobrazit žebříček kategorie",
"versusAutoVerdict": "Automatický verdikt: {winner} vede s celkovým skóre {score}/5,0{expertSuffix}.",
"versusAutoVerdict": "Automatický verdikt: {winner} vede s celkovým skóre {score}{expertSuffix}.",
"mdTieLine": "Remíza ({score}/5,0)",
"tieDeltaNote": "(stejné skóre při aktuálních vahách)",
"tie1v1Badge": "Remíza 1v1",
"versusAutoVerdictTie": "Automatický verdikt: oba modely mají stejné celkové skóre ({score}/5,0); rozhodněte podle ceny a parametrů, které potřebujete.",
"versusAutoVerdictTie": "Automatický verdikt: oba modely mají stejné celkové skóre ({score}); rozhodněte podle ceny a parametrů, které potřebujete.",
"versusVerdictWeights": "(Váhy: {weights}.)",
"compareMetaTitleWithNames": "Porovnání {names} - Hodnocení a 10 skupin parametrů | Rate.vn",
"compareMetaTitleWithNamesShort": "{names} | Rate.vn",
Expand Down Expand Up @@ -201,7 +201,7 @@
"catEarbudsUnit": "modelů sluchátek",
"boolYes": "Ano",
"boolNo": "Ne",
"group0Title": "0. Hodnocení a důvěryhodnost",
"group0Title": "Hodnocení a důvěryhodnost",
"rowRatingWeighted": "Skutečné hodnocení kupujících (bez falešných recenzí)",
"valAuthenticReviews": "★ {rating} / 5,0 ({count, plural, one {# ověřená recenze} few {# ověřené recenze} other {# ověřených recenzí}})",
"valNoReviews": "Zatím bez hodnocení",
Expand Down Expand Up @@ -229,7 +229,7 @@
"rowCamSecond": "Druhý fotoaparát (Ultraširokoúhlý)",
"rowCamThirdFourth": "Třetí a čtvrtý fotoaparát (Tele / Periskop)",
"rowCamVideo": "Záznam videa a blesk",
"rowCamFront": "Přední kamera (Selfie)",
"rowCamFront": "Přední kamera",
"rowBatCapacity": "Kapacita baterie",
"rowBatFastCharge": "Výkon rychlého nabíjení",
"rowBatDetails": "Technologie baterie, bezdrátové a reverzní nabíjení",
Expand Down Expand Up @@ -290,7 +290,7 @@
"verdictSummaryHeading": "Shrnutí výhod mezi {count} porovnávanými {category}",
"viewAllCategoryBtn": "Zobrazit vše v {category} →",
"verdictHighestRatedTitle": "Nejvyšší ověřené hodnocení",
"verdictAiFilteredNote": "(bez falešných recenzí)",
"verdictAiFilteredNote": "Bez falešných recenzí",
"verdictTopHardwareTitle": "Nejlepší hardwarová výbava",
"verdictNormalized10Groups": "Standardizováno 10 skupin",
"verdictBestBatteryTitle": "Výdrž baterie a energie",
Expand Down Expand Up @@ -402,7 +402,6 @@
"rank2Label": "2. místo",
"rank3Label": "3. místo",
"fullTableTitle": "Kompletní žebříček kategorie",
"verifiedReviewsSuffix": "ověřených recenzí",
"blockedSpamCount": "{count, plural, one {Zablokována # falešná či spamová recenze} few {Zablokovány # falešné či spamové recenze} other {Zablokováno # falešných či spamových recenzí}}",
"storesTrackedSuffix": "obchodů",
"viewAuditDetailBtn": "Recenze a 10 skupin parametrů →",
Expand Down Expand Up @@ -437,7 +436,6 @@
"categoryTabsAria": "Vyberte kategorii",
"shortCamera": "Foto",
"shortAnc": "ANC",
"userScoreShort": "Uživatelé {score}/5,0",
"expertScoreShort": "Expert {score}/10"
},
"reviews": {
Expand Down Expand Up @@ -507,7 +505,6 @@
"authenticUserScoreTitle": "Skutečné hodnocení kupujících",
"basedOnAuthenticReviews": "Vypočteno z {count, plural, one {# skutečné recenze} other {# skutečných recenzí}} (Σw = {weightSum} · Vážený průměr: {avgScore}).",
"bayesianConfidenceTooltip": "Bayesovská spolehlivost c = Σw / (Σw + 3) = {pct} %",
"validCountBadge": "✓ {count, plural, one {# platná} few {# platné} other {# platných}}",
"blockedSeedingBadge": "{count, plural, one {Odfiltrována # falešná nebo spamová recenze} few {Odfiltrovány # falešné nebo spamové recenze} other {Odfiltrováno # falešných nebo spamových recenzí}}",
"expertLabScoreTitle": "Expertní skóre Rate Lab",
"expertPendingShort": "nehodnoceno",
Expand Down Expand Up @@ -946,7 +943,7 @@
"listingsSectionSubtitle": "Porovnání cen u prodejce <value>{name}</value> s nejnižší tržní cenou a historickým mediánem",
"noListingsYet": "K tomuto obchodu zatím nejsou přiřazeny žádné produkty.",
"colListedProduct": "Nabízený produkt",
"colMasterProduct": "Standardizovaný model (Master Product)",
"colMasterProduct": "Standardizovaný model",
"colRecordedPrice": "Zaznamenaná cena",
"colPromoAudit": "Sleva (podle historie cen)",
"colAuditAndSource": "Kontrola a zdroj",
Expand Down Expand Up @@ -1019,13 +1016,11 @@
"scoreboardEyebrow": "Hodnocení bez falešných recenzí",
"authenticReviewsBadge": "{count, plural, one {# autentická recenze} few {# autentické recenze} other {# autentických recenzí}}",
"filteredSeedingBadge": "{count, plural, one {Odfiltrována # falešná nebo spamová recenze} few {Odfiltrovány # falešné nebo spamové recenze} other {Odfiltrováno # falešných nebo spamových recenzí}}",
"userWeightedScoreLabel": "Skóre skutečných kupujících (vážené)",
"userWeightedScoreLabel": "Skóre kupujících",
"userScoreHint": "Vážený průměr bez falešných a spamových recenzí",
"notAvailableYet": "Není",
"verifiedReviewsCount": "/ 5,0 ({count, plural, one {# ověřená recenze} few {# ověřené recenze} other {# ověřených recenzí}})",
"verifiedReviewsCountNoScore": "({count, plural, one {# ověřená recenze} few {# ověřené recenze} other {# ověřených recenzí}})",
"expertScoreLabel": "Expertní skóre Rate Lab",
"tenGroupStandardWithSlash": "/ 5,0 dle 10 skupin",
"tenGroupStandard": "standard 10 skupin",
"expertScoreHint": "Z 10, hodnoceno podle 10 skupin parametrů",
"critSoundstage": "Zvukový projev a kvalita",
"critDisplay": "Displej a zobrazení",
"critPerformance": "Výkon a teploty",
Expand Down Expand Up @@ -1796,11 +1791,11 @@
"jreviewsRatingHeader": "Hodnocení (JReviews)",
"ratingAll": "Vše",
"categoryHeader": "Kategorie zařízení",
"brandHeader": "Výrobce (Brand)",
"brandHeader": "Výrobce",
"priceRangeHeader": "Cenové rozpětí (Miliony VND)",
"ramHeader": "Kapacita RAM (Hardware)",
"ramHeader": "Kapacita RAM",
"storageHeader": "Interní úložiště (ROM)",
"featuresHeader": "Klíčové funkce (10 skupin parametrů)",
"featuresHeader": "Klíčové funkce",
"filteringBtn": "Filtrování dat...",
"applyFiltersBtn": "Použít filtry",
"clearAllFiltersBtn": "Vymazat všechny filtry",
Expand Down Expand Up @@ -1925,8 +1920,12 @@
"okOtpSent": "6místný OTP kód byl odeslán na váš e-mail.",
"okOtpVerified": "OTP kód byl úspěšně ověřen! Přesměrování..."
},
"starRating": {
"aria": "{value} z 5 hvězdiček",
"ariaWithCount": "{value} z 5 hvězdiček, {countText} hodnocení",
"empty": "Zatím bez hodnocení"
},
"productCard": {
"reviewCount": "{count, plural, one {# recenze} few {# recenze} other {# recenzí}}",
"noReviews": "Zatím bez recenzí",
"expertScore": "Lab {score}",
"refPrice": "Orientační cena",
Expand Down
Loading
Loading