Skip to content

Commit 5fdd152

Browse files
authored
bug: both teams' header rows in the match stats tabs look the same (#914)
1 parent 702170d commit 5fdd152

6 files changed

Lines changed: 211 additions & 32 deletions

File tree

‎components/common/StatLabel.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const headerTouch = computed(
5252
<template #trigger>
5353
<button
5454
type="button"
55+
data-stat-info
5556
class="-m-1 inline-flex shrink-0 items-center justify-center p-2 text-muted-foreground/70 hover:text-foreground"
5657
:aria-label="title"
5758
>

‎components/match/LineupAimStats.vue‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,10 @@ const { sortKey, sortDir, toggle, sortRows } = useTableSort<string>();
4242

4343
<template>
4444
<Table
45-
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5"
45+
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5 [&>tbody:not(:last-child)>tr:last-child]:border-b-[3px] [&>tbody:not(:last-child)>tr:last-child]:border-border/80"
4646
>
47-
<template v-for="(lp, lpIdx) of lineupsToRender" :key="lp.id">
48-
<TableHeader
49-
:class="[
50-
'[&_th]:h-12 bg-muted/20',
51-
lpIdx > 0 ? '[&_th]:pt-7 border-t-[3px] border-border/80' : '',
52-
]"
53-
>
47+
<template v-for="lp of lineupsToRender" :key="lp.id">
48+
<TableHeader class="[&_th]:h-12 bg-muted/20">
5449
<TableRow>
5550
<TableHead
5651
v-if="!hideMember"

‎components/match/LineupOpeningDuels.vue‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,10 @@ function sortGettersFor(lp: any): Record<string, (m: any) => unknown> {
216216
</script>
217217
<template>
218218
<Table
219-
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5"
219+
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5 [&>tbody:not(:last-child)>tr:last-child]:border-b-[3px] [&>tbody:not(:last-child)>tr:last-child]:border-border/80"
220220
>
221-
<template v-for="(lp, lpIdx) of lineupsToRender" :key="lp.id">
222-
<TableHeader
223-
:class="[
224-
'[&_th]:h-12 bg-muted/20',
225-
lpIdx > 0 ? '[&_th]:pt-7 border-t-[3px] border-border/80' : '',
226-
]"
227-
>
221+
<template v-for="lp of lineupsToRender" :key="lp.id">
222+
<TableHeader class="[&_th]:h-12 bg-muted/20">
228223
<TableRow>
229224
<TableHead
230225
class="w-[110px] md:w-[220px] text-left whitespace-nowrap sticky left-0 z-20 bg-card border-r border-border [transform:translateZ(0)]"

‎components/match/LineupOverview.vue‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,10 @@ import {
143143

144144
<template>
145145
<Table
146-
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5"
146+
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5 [&_th:not(.sticky)_[data-stat-info]]:order-last [&>tbody:not(:last-child)>tr:last-child]:border-b-[3px] [&>tbody:not(:last-child)>tr:last-child]:border-border/80"
147147
>
148-
<template v-for="(lp, lpIdx) of lineupsToRender" :key="lp.id">
149-
<TableHeader
150-
:class="[
151-
'[&_th]:h-12 bg-muted/20',
152-
lpIdx > 0 ? '[&_th]:pt-7 border-t-[3px] border-border/80' : '',
153-
]"
154-
>
148+
<template v-for="lp of lineupsToRender" :key="lp.id">
149+
<TableHeader class="[&_th]:h-12 bg-muted/20">
155150
<TableRow>
156151
<TableHead
157152
v-if="!hideMember"

‎components/match/LineupTradeStats.vue‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,10 @@ const tradeColumns = computed(() =>
7171

7272
<template>
7373
<Table
74-
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5"
74+
class="min-w-full w-max [&_td]:whitespace-nowrap [&_th]:px-2 [&_td]:px-2 [&_th.sticky+th]:!pl-5 [&_td.sticky+td]:!pl-5 [&>tbody:not(:last-child)>tr:last-child]:border-b-[3px] [&>tbody:not(:last-child)>tr:last-child]:border-border/80"
7575
>
76-
<template v-for="(lp, lpIdx) of lineupsToRender" :key="lp.id">
77-
<TableHeader
78-
:class="[
79-
'[&_th]:h-12 bg-muted/20',
80-
lpIdx > 0 ? '[&_th]:pt-7 border-t-[3px] border-border/80' : '',
81-
]"
82-
>
76+
<template v-for="lp of lineupsToRender" :key="lp.id">
77+
<TableHeader class="[&_th]:h-12 bg-muted/20">
8378
<TableRow>
8479
<TableHead
8580
v-if="!hideMember"
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { createRequire } from "node:module";
3+
import { resolve } from "node:path";
4+
import { defineComponent, h, type Component } from "vue";
5+
import { flushPromises } from "@vue/test-utils";
6+
import { mountSuspended } from "@nuxt/test-utils/runtime";
7+
import postcss from "postcss";
8+
import tailwindcss from "tailwindcss";
9+
import LineupOverview from "~/components/match/LineupOverview.vue";
10+
import LineupAimStats from "~/components/match/LineupAimStats.vue";
11+
import LineupOpeningDuels from "~/components/match/LineupOpeningDuels.vue";
12+
import LineupTradeStats from "~/components/match/LineupTradeStats.vue";
13+
import { TooltipProvider } from "~/components/ui/tooltip";
14+
import { e_match_status_enum } from "~/generated/zeus";
15+
16+
vi.mock("@vue/apollo-composable", async (importOriginal) => ({
17+
...(await importOriginal<typeof import("@vue/apollo-composable")>()),
18+
useApolloClient: () => ({
19+
client: {
20+
subscribe: () => ({ subscribe: () => ({ unsubscribe: () => {} }) }),
21+
},
22+
}),
23+
}));
24+
25+
const loadConfig = createRequire(import.meta.url)("tailwindcss/loadConfig");
26+
const tailwindConfig = loadConfig(
27+
resolve(process.cwd(), "tailwind.config.js"),
28+
);
29+
30+
const row = defineComponent({
31+
render: () => h("tr", [h("td", "player")]),
32+
});
33+
34+
const stubs = {
35+
LineupOverviewRow: row,
36+
LineupOpeningDuelRow: row,
37+
LineupMember: true,
38+
};
39+
40+
const player = (steamId: string) => ({
41+
steam_id: steamId,
42+
player: { steam_id: steamId, name: `Player ${steamId}` },
43+
});
44+
45+
const lineup = (id: string, name: string, steamIds: string[]) => ({
46+
id,
47+
name,
48+
is_ready: true,
49+
lineup_players: steamIds.map(player),
50+
});
51+
52+
const lineup1 = lineup("lineup-1", "Northwind", ["1", "2"]);
53+
const lineup2 = lineup("lineup-2", "Iron Lotus", ["3", "4"]);
54+
55+
const match = {
56+
id: "match-1",
57+
status: e_match_status_enum.Finished,
58+
min_players_per_lineup: 2,
59+
max_players_per_lineup: 2,
60+
lineup_1: lineup1,
61+
lineup_2: lineup2,
62+
match_maps: [],
63+
};
64+
65+
const tabs: Array<[string, Component]> = [
66+
["Overview", LineupOverview],
67+
["Aim", LineupAimStats],
68+
["Opening Duels", LineupOpeningDuels],
69+
["Trades", LineupTradeStats],
70+
];
71+
72+
let unmount: (() => void) | undefined;
73+
74+
afterEach(() => {
75+
unmount?.();
76+
unmount = undefined;
77+
document.head.querySelector("style[data-lineup-tables]")?.remove();
78+
vi.restoreAllMocks();
79+
});
80+
81+
// Built from the classes the table rendered, so the real cascade decides each
82+
// border, the table body's own last-row border-0 included. Read off the
83+
// elements: serialised HTML escapes the `&` in every arbitrary variant, and
84+
// Tailwind would build rules for names nothing carries.
85+
async function applyTailwind(root: Element) {
86+
const classes = [root, ...root.querySelectorAll("[class]")].map(
87+
(element) => element.getAttribute("class") ?? "",
88+
);
89+
const { css } = await postcss([
90+
tailwindcss({
91+
...tailwindConfig,
92+
content: [{ raw: classes.join(" "), extension: "html" }],
93+
}),
94+
]).process("@tailwind base;\n@tailwind utilities;", { from: undefined });
95+
const style = document.createElement("style");
96+
style.dataset.lineupTables = "";
97+
style.textContent = css;
98+
document.head.appendChild(style);
99+
}
100+
101+
async function mountTable(
102+
component: Component,
103+
props: Record<string, unknown>,
104+
) {
105+
const wrapper = await mountSuspended(
106+
defineComponent({
107+
render: () =>
108+
h(TooltipProvider, null, () =>
109+
h(component, { match, lineup: lineup1, ...props }),
110+
),
111+
}),
112+
{ global: { stubs }, attachTo: document.body },
113+
);
114+
unmount = () => wrapper.unmount();
115+
await flushPromises();
116+
const table = wrapper.find("table").element as HTMLTableElement;
117+
await applyTailwind(table);
118+
return table;
119+
}
120+
121+
function sections(table: HTMLTableElement, tag: "THEAD" | "TBODY") {
122+
return [...table.children].filter((child) => child.tagName === tag);
123+
}
124+
125+
function lastRowBorder(body: Element) {
126+
const rows = body.querySelectorAll(":scope > tr");
127+
return getComputedStyle(rows[rows.length - 1]).borderBottomWidth;
128+
}
129+
130+
describe.each(tabs)("%s tab with both teams", (_name, component) => {
131+
it("gives the second team's header row exactly the first team's", async () => {
132+
const table = await mountTable(component, { combineWith: lineup2 });
133+
const [first, second] = sections(table, "THEAD");
134+
135+
expect(second.className).toBe(first.className);
136+
137+
const firstHead = getComputedStyle(first.querySelector("th")!);
138+
const secondHead = getComputedStyle(second.querySelector("th")!);
139+
expect(secondHead.paddingTop).toBe(firstHead.paddingTop);
140+
expect(getComputedStyle(second).borderTopWidth).toBe(
141+
getComputedStyle(first).borderTopWidth,
142+
);
143+
});
144+
145+
it("draws the 3px line under the first team's last player only", async () => {
146+
const table = await mountTable(component, { combineWith: lineup2 });
147+
const [first, second] = sections(table, "TBODY");
148+
149+
expect(lastRowBorder(first)).toBe("3px");
150+
expect(lastRowBorder(second)).toBe("0px");
151+
});
152+
});
153+
154+
describe("a one-team table", () => {
155+
it("has no separating line", async () => {
156+
const table = await mountTable(LineupOverview, {});
157+
const [only] = sections(table, "TBODY");
158+
159+
expect(sections(table, "THEAD")).toHaveLength(1);
160+
expect(lastRowBorder(only)).toBe("0px");
161+
});
162+
});
163+
164+
describe("Overview headings on a touch screen", () => {
165+
function touchScreen() {
166+
vi.spyOn(window, "matchMedia").mockImplementation(
167+
(query: string) =>
168+
({
169+
matches: query.includes("hover: none"),
170+
media: query,
171+
onchange: null,
172+
addEventListener: () => {},
173+
removeEventListener: () => {},
174+
addListener: () => {},
175+
removeListener: () => {},
176+
dispatchEvent: () => false,
177+
}) as MediaQueryList,
178+
);
179+
}
180+
181+
it("puts the info icon after the label, so the label starts over its numbers", async () => {
182+
touchScreen();
183+
const table = await mountTable(LineupOverview, { combineWith: lineup2 });
184+
const infos = [
185+
...table.querySelectorAll("th:not(.sticky) button"),
186+
].filter((button) => button.querySelector("svg.lucide-info"));
187+
188+
expect(infos.length).toBeGreaterThan(0);
189+
for (const info of infos) {
190+
let box = info.parentElement!;
191+
while (getComputedStyle(box).display === "contents") {
192+
box = box.parentElement!;
193+
}
194+
expect(getComputedStyle(box).display).toBe("inline-flex");
195+
expect(getComputedStyle(info).order).toBe("9999");
196+
}
197+
});
198+
});

0 commit comments

Comments
 (0)