AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
1import { describe, it, expect } from 'vitest';
2import {
3 buildShareText,
4 buildShareFacets,
5 assembleShareText,
6 defaultShareNote,
7 countGraphemes,
8 POST_MAX_GRAPHEMES,
9 emojiGrid,
10 shareHeader,
11} from '../src/engine/share.js';
12import type { GameResult } from '../src/engine/types.js';
13
14const win: GameResult = {
15 lang: 'fr',
16 puzzleNumber: 412,
17 solved: true,
18 guessCount: 4,
19 rows: [
20 ['absent', 'present', 'absent', 'absent', 'present'],
21 ['absent', 'correct', 'absent', 'present', 'absent'],
22 ['correct', 'correct', 'absent', 'present', 'present'],
23 ['correct', 'correct', 'correct', 'correct', 'correct'],
24 ],
25};
26
27describe('share text', () => {
28 it('header is "AT Mot <n> (<LANG>) <score>"', () => {
29 expect(shareHeader(win)).toBe('AT Mot 412 (FR) 4/6');
30 });
31
32 it('uses standard Unicode squares (not brand colours)', () => {
33 expect(emojiGrid(win.rows).split('\n')[3]).toBe('🟩🟩🟩🟩🟩');
34 expect(emojiGrid([['absent', 'present', 'correct', 'absent', 'absent']])).toBe('⬛🟨🟩⬛⬛');
35 });
36
37 it('includes the puzzle permalink (load-bearing for Constellation discovery)', () => {
38 const text = buildShareText(win);
39 expect(text).toContain('https://atmot.herve.bzh/p/fr/412');
40 // header, blank, grid, blank, link
41 const parts = text.split('\n\n');
42 expect(parts[0]).toBe('AT Mot 412 (FR) 4/6');
43 expect(parts[2]).toBe('https://atmot.herve.bzh/p/fr/412');
44 });
45
46 it('shows X/6 on a loss', () => {
47 const loss: GameResult = { ...win, solved: false, guessCount: undefined, rows: win.rows.slice(0, 6) };
48 expect(shareHeader(loss)).toContain('X/6');
49 });
50
51 it('never contains the word "Wordle"', () => {
52 expect(buildShareText(win).toLowerCase()).not.toContain('wordle');
53 });
54});
55
56describe('custom share message', () => {
57 it('defaultShareNote is the header line', () => {
58 expect(defaultShareNote(win)).toBe('AT Mot 412 (FR) 4/6');
59 });
60
61 it('assembleShareText keeps grid + link, with the note first', () => {
62 const text = assembleShareText(win, 'I nailed it 🎉');
63 const parts = text.split('\n\n');
64 expect(parts[0]).toBe('I nailed it 🎉');
65 expect(parts[parts.length - 1]).toBe('https://atmot.herve.bzh/p/fr/412');
66 expect(text).toContain(emojiGrid(win.rows));
67 });
68
69 it('assembleShareText drops the note line when the note is empty', () => {
70 const text = assembleShareText(win, ' ');
71 expect(text).not.toContain('AT Mot');
72 expect(text.split('\n\n')[0]).toBe(emojiGrid(win.rows));
73 expect(text).toContain('https://atmot.herve.bzh/p/fr/412');
74 });
75
76 it('buildShareText is unchanged (default note = header)', () => {
77 expect(buildShareText(win)).toBe(assembleShareText(win, shareHeader(win)));
78 });
79
80 it('buildShareFacets recomputes byte offsets for a custom note', () => {
81 const note = 'Réussi ! 🎉 accents éàü';
82 const text = assembleShareText(win, note);
83 const facet = buildShareFacets(win, note)[0]!;
84 const bytes = new TextEncoder().encode(text);
85 const slice = new TextDecoder().decode(bytes.slice(facet.index.byteStart, facet.index.byteEnd));
86 expect(slice).toBe('https://atmot.herve.bzh/p/fr/412');
87 });
88
89 it('countGraphemes treats each emoji square as one', () => {
90 expect(countGraphemes('🟩🟩🟩')).toBe(3);
91 expect(countGraphemes('abc')).toBe(3);
92 expect(POST_MAX_GRAPHEMES).toBe(300);
93 });
94});