feat: improve opening trainer flow and add deviation dialog
- Add tutor message guardrail to prevent rapid-fire messages - Track last message by move index - Wait for both player and opponent moves before speaking - Speak immediately when player deviates from theory - Add deviation dialog with three options: - Continue Playing (Start Game) - transitions to game mode - Undo & Return to Opening - returns to theory - Explore This Variation - continues off-book practice - Implement smooth game mode transition: - Pass opening context from trainer to game - Tutor welcomes player with context about their study - Computer makes first move if needed in starting position - Fix generate-test-fixtures script: - Add safety check to prevent overwriting real data - Restore full opening database (12,379 openings) - Rebuild move index (12,377 sequences) - Add clear warnings about test fixtures - Update ChessGame to accept openingContext prop - Update Tutor to display contextual greeting in game mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"rn1qkbnr/ppp2ppp/8/3p4/5p2/6PB/PPPPP2P/RNBQK2R w KQkq - 0 5": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4",
|
||||
"name": "Amar Gambit"
|
||||
},
|
||||
"rnbqkbnr/pppppppp/8/8/8/7N/PPPPPPPP/RNBQKB1R b KQkq - 1 1": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3",
|
||||
"name": "Amar Opening",
|
||||
"scid": "A00g",
|
||||
"aliases": {
|
||||
"scid": "Amar/Paris Opening",
|
||||
"eco_wikip": "Irregular Openings: Amar Opening",
|
||||
"ct": "Amar Opening, General",
|
||||
"chessGraph": "Amar; Paris Opening",
|
||||
"chronos": "Amar (Paris) opening",
|
||||
"icsbot": "Amar; Paris Opening "
|
||||
}
|
||||
},
|
||||
"rn1qkbnr/ppp2ppp/8/3p4/8/6PB/PPPPP3/RNBQ1RK1 b kq - 0 6": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4 5. O-O fxg3 6. hxg3",
|
||||
"name": "Amar Opening: Paris Gambit, Gent Gambit",
|
||||
"aliases": {
|
||||
"ct": "Amar Opening, Gent Gambit"
|
||||
}
|
||||
},
|
||||
"rnbqkbnr/ppp2ppp/8/3pp3/5P2/6PN/PPPPP2P/RNBQKB1R b KQkq - 0 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4",
|
||||
"name": "Amar Opening: Paris Gambit",
|
||||
"scid": "A00g",
|
||||
"aliases": {
|
||||
"scid": "Amar: Paris Gambit"
|
||||
}
|
||||
},
|
||||
"r1bqkb1r/ppp2ppp/2np1n2/4p3/2P5/1PN1P3/P2P1PPP/R1BQKBNR w KQkq - 1 5": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. e3 e5 2. c4 d6 3. Nc3 Nc6 4. b3 Nf6",
|
||||
"name": "Amsterdam Attack"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"rnbqkbnr/ppppp1pp/5p2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 f6",
|
||||
"name": "Barnes Defense"
|
||||
},
|
||||
"rnbqkbnr/1pppppp1/p6p/8/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3": {
|
||||
"src": "scid",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 a6 2. d4 h6",
|
||||
"name": "Basman's Creepy-Crawly System (as Black)",
|
||||
"scid": "B00c"
|
||||
},
|
||||
"rnbqkbnr/pppppp1p/8/6p1/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5",
|
||||
"name": "Borg Defense",
|
||||
"scid": "B00b",
|
||||
"aliases": {
|
||||
"scid": "Reversed Grob (Borg/Basman Defence)",
|
||||
"ct": "Borg Defense, General",
|
||||
"chessGraph": "Basman Defense; Macho Grob",
|
||||
"chronos": "Reversed Grob (Borg/Basman defence/macho Grob)",
|
||||
"icsbot": "Basman Defense; Macho Grob "
|
||||
}
|
||||
},
|
||||
"rnbqk1nr/ppppppbp/8/6p1/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 1 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5 2. d4 Bg7",
|
||||
"name": "Borg Defense: Borg Gambit",
|
||||
"aliases": {
|
||||
"eco_js": "Borg Defense: Borg Gambit, 2. d4 Bg7",
|
||||
"chessGraph": "Borg Gambit",
|
||||
"icsbot": "Borg Gambit "
|
||||
}
|
||||
},
|
||||
"rnbqkbnr/p2p1p1p/1p2p3/2P3p1/4P3/2P5/PP3PPP/RNBQKBNR w KQkq - 0 5": {
|
||||
"src": "eco_js",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5 2. d4 e6 3. c3 c5 4. dxc5 b6",
|
||||
"name": "Borg Defense: Langhorst Gambit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"rnbqkbnr/pppp1ppp/4p3/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6",
|
||||
"name": "French Defense",
|
||||
"scid": "C00a",
|
||||
"aliases": {
|
||||
"scid": "French",
|
||||
"ct": "French Defense, General",
|
||||
"chessGraph": "French Defense; C00",
|
||||
"icsbot": "French Defense; C00 "
|
||||
},
|
||||
"isEcoRoot": true,
|
||||
"wikipediaSlug": "french-defense"
|
||||
},
|
||||
"rnbqkbnr/ppp2ppp/4p3/3p4/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 d5",
|
||||
"name": "French Defense",
|
||||
"scid": "C00x",
|
||||
"aliases": {
|
||||
"scid": "French: 2.d4 d5",
|
||||
"eco_js": "French Defense, 2. d4 d5",
|
||||
"ct": "French Defense, Main Line"
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
},
|
||||
"rnbqkbnr/ppp2ppp/4p3/3p4/3PP3/4B3/PPP2PPP/RN1QKBNR b KQkq - 1 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 d5 3. Be3",
|
||||
"name": "French Defense: Alapin Gambit",
|
||||
"scid": "C00x",
|
||||
"aliases": {
|
||||
"scid": "French: Alapin-Diemer Gambit (ADG)",
|
||||
"chessGraph": "Alapin; Witaker Gambit, French",
|
||||
"chronos": "French: Alapin variation",
|
||||
"icsbot": "Alapin; Witaker Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
},
|
||||
"rnbqkbnr/p1pp1ppp/4p3/1p6/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 b5",
|
||||
"name": "French Defense: Baeuerle Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Baeuerle Gambit, French",
|
||||
"icsbot": "Baeuerle Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
},
|
||||
"rnbqkbnr/pppp1ppp/4p3/8/1P2P3/8/P1PP1PPP/RNBQKBNR b KQkq - 0 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. b4",
|
||||
"name": "French Defense: Banzai-Leong Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Banzai-Leong Gambit, French",
|
||||
"icsbot": "Banzai-Leong Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"rnbqkbnr/ppp1pppp/8/3p4/3P4/3Q4/PPP1PPPP/RNB1KBNR b KQkq - 1 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. Qd3",
|
||||
"name": "Amazon Attack"
|
||||
},
|
||||
"rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4",
|
||||
"name": "Blackmar-Diemer Gambit",
|
||||
"scid": "D00l",
|
||||
"aliases": {
|
||||
"scid": "Blackmar-Diemer Gambit (BDG): 2.e4",
|
||||
"ct": "Blackmar-Diemer Gambit, General",
|
||||
"chronos": "Blackmar gambit"
|
||||
}
|
||||
},
|
||||
"rnbqkb1r/ppp1pppp/5n2/8/3Pp3/2N5/PPP2PPP/R1BQKBNR w KQkq - 2 4": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4 3. Nc3 Nf6",
|
||||
"name": "Blackmar-Diemer Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Vienna Variation, BDG",
|
||||
"icsbot": "Vienna Variation, BDG "
|
||||
}
|
||||
},
|
||||
"rnbqkbnr/ppp1pppp/8/8/3Pp3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3": {
|
||||
"src": "scid",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4",
|
||||
"name": "Blackmar-Diemer Gambit (BDG): 2.e4 dxe4",
|
||||
"scid": "D00l"
|
||||
},
|
||||
"rnbqkb1r/ppp1pppp/5n2/8/3P4/2N2p2/PPP3PP/R1BQKBNR w KQkq - 0 5": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4 3. Nc3 Nf6 4. f3 exf3",
|
||||
"name": "Blackmar-Diemer Gambit Accepted",
|
||||
"scid": "D00v",
|
||||
"aliases": {
|
||||
"scid": "Blackmar-Diemer: Accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"rnbqkb1r/pppp1ppp/4pn2/8/2PP4/6P1/PP2PP1P/RNBQKBNR b KQkq - 0 3": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3",
|
||||
"name": "Catalan Opening",
|
||||
"scid": "E00g",
|
||||
"aliases": {
|
||||
"scid": "Catalan",
|
||||
"ct": "Catalan Opening, General"
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
},
|
||||
"rnbqkb1r/ppp2ppp/4pn2/3p4/2PP4/6P1/PP2PP1P/RNBQKBNR w KQkq - 0 4": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 d5",
|
||||
"name": "Catalan Opening",
|
||||
"scid": "E00n",
|
||||
"aliases": {
|
||||
"scid": "Catalan: 3...d5",
|
||||
"eco_js": "Catalan Opening, 2. c4 e6 3. g3 d5",
|
||||
"chessGraph": "E01"
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
},
|
||||
"rnbqkb1r/pppp1ppp/5n2/4p3/2PP4/6P1/PP2PP1P/RNBQKBNR w KQkq - 0 4": {
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 e5",
|
||||
"name": "Catalan Opening: Hungarian Gambit",
|
||||
"scid": "E00g",
|
||||
"aliases": {
|
||||
"scid": "Catalan: Hungarian Gambit",
|
||||
"chessGraph": "Hungarian Gambit, Catalan",
|
||||
"icsbot": "Hungarian Gambit, Catalan "
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
},
|
||||
"rnbqk2r/pppp1ppp/4pn2/8/1bPP4/6P1/PP2PP1P/RNBQKBNR w KQkq - 1 4": {
|
||||
"src": "scid",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 Bb4+",
|
||||
"name": "Catalan: 3...Bb4+",
|
||||
"scid": "E00i",
|
||||
"wikipediaSlug": "catalan"
|
||||
},
|
||||
"rnbqk2r/pppp1ppp/4pn2/8/1bPP4/6P1/PP1BPP1P/RN1QKBNR b KQkq - 2 4": {
|
||||
"src": "scid",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 Bb4+ 4. Bd2",
|
||||
"name": "Catalan: 3...Bb4+ 4.Bd2",
|
||||
"scid": "E00j",
|
||||
"wikipediaSlug": "catalan"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4",
|
||||
"name": "Amar Gambit"
|
||||
}
|
||||
],
|
||||
"1. Nh3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3",
|
||||
"name": "Amar Opening",
|
||||
"scid": "A00g",
|
||||
"aliases": {
|
||||
"scid": "Amar/Paris Opening",
|
||||
"eco_wikip": "Irregular Openings: Amar Opening",
|
||||
"ct": "Amar Opening, General",
|
||||
"chessGraph": "Amar; Paris Opening",
|
||||
"chronos": "Amar (Paris) opening",
|
||||
"icsbot": "Amar; Paris Opening "
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4 5. O-O fxg3 6. hxg3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4 Bxh3 4. Bxh3 exf4 5. O-O fxg3 6. hxg3",
|
||||
"name": "Amar Opening: Paris Gambit, Gent Gambit",
|
||||
"aliases": {
|
||||
"ct": "Amar Opening, Gent Gambit"
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. Nh3 d5 2. g3 e5 3. f4": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. Nh3 d5 2. g3 e5 3. f4",
|
||||
"name": "Amar Opening: Paris Gambit",
|
||||
"scid": "A00g",
|
||||
"aliases": {
|
||||
"scid": "Amar: Paris Gambit"
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. e3 e5 2. c4 d6 3. Nc3 Nc6 4. b3 Nf6": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "A00",
|
||||
"moves": "1. e3 e5 2. c4 d6 3. Nc3 Nc6 4. b3 Nf6",
|
||||
"name": "Amsterdam Attack"
|
||||
}
|
||||
],
|
||||
"1. e4 f6": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 f6",
|
||||
"name": "Barnes Defense"
|
||||
}
|
||||
],
|
||||
"1. e4 a6 2. d4 h6": [
|
||||
{
|
||||
"src": "scid",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 a6 2. d4 h6",
|
||||
"name": "Basman's Creepy-Crawly System (as Black)",
|
||||
"scid": "B00c"
|
||||
}
|
||||
],
|
||||
"1. e4 g5": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5",
|
||||
"name": "Borg Defense",
|
||||
"scid": "B00b",
|
||||
"aliases": {
|
||||
"scid": "Reversed Grob (Borg/Basman Defence)",
|
||||
"ct": "Borg Defense, General",
|
||||
"chessGraph": "Basman Defense; Macho Grob",
|
||||
"chronos": "Reversed Grob (Borg/Basman defence/macho Grob)",
|
||||
"icsbot": "Basman Defense; Macho Grob "
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. e4 g5 2. d4 Bg7": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5 2. d4 Bg7",
|
||||
"name": "Borg Defense: Borg Gambit",
|
||||
"aliases": {
|
||||
"eco_js": "Borg Defense: Borg Gambit, 2. d4 Bg7",
|
||||
"chessGraph": "Borg Gambit",
|
||||
"icsbot": "Borg Gambit "
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. e4 g5 2. d4 e6 3. c3 c5 4. dxc5 b6": [
|
||||
{
|
||||
"src": "eco_js",
|
||||
"eco": "B00",
|
||||
"moves": "1. e4 g5 2. d4 e6 3. c3 c5 4. dxc5 b6",
|
||||
"name": "Borg Defense: Langhorst Gambit"
|
||||
}
|
||||
],
|
||||
"1. e4 e6": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6",
|
||||
"name": "French Defense",
|
||||
"scid": "C00a",
|
||||
"aliases": {
|
||||
"scid": "French",
|
||||
"ct": "French Defense, General",
|
||||
"chessGraph": "French Defense; C00",
|
||||
"icsbot": "French Defense; C00 "
|
||||
},
|
||||
"isEcoRoot": true,
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
],
|
||||
"1. e4 e6 2. d4 d5": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 d5",
|
||||
"name": "French Defense",
|
||||
"scid": "C00x",
|
||||
"aliases": {
|
||||
"scid": "French: 2.d4 d5",
|
||||
"eco_js": "French Defense, 2. d4 d5",
|
||||
"ct": "French Defense, Main Line"
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
],
|
||||
"1. e4 e6 2. d4 d5 3. Be3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 d5 3. Be3",
|
||||
"name": "French Defense: Alapin Gambit",
|
||||
"scid": "C00x",
|
||||
"aliases": {
|
||||
"scid": "French: Alapin-Diemer Gambit (ADG)",
|
||||
"chessGraph": "Alapin; Witaker Gambit, French",
|
||||
"chronos": "French: Alapin variation",
|
||||
"icsbot": "Alapin; Witaker Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
],
|
||||
"1. e4 e6 2. d4 b5": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. d4 b5",
|
||||
"name": "French Defense: Baeuerle Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Baeuerle Gambit, French",
|
||||
"icsbot": "Baeuerle Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
],
|
||||
"1. e4 e6 2. b4": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "C00",
|
||||
"moves": "1. e4 e6 2. b4",
|
||||
"name": "French Defense: Banzai-Leong Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Banzai-Leong Gambit, French",
|
||||
"icsbot": "Banzai-Leong Gambit, French "
|
||||
},
|
||||
"wikipediaSlug": "french-defense"
|
||||
}
|
||||
],
|
||||
"1. d4 d5 2. Qd3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. Qd3",
|
||||
"name": "Amazon Attack"
|
||||
}
|
||||
],
|
||||
"1. d4 d5 2. e4": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4",
|
||||
"name": "Blackmar-Diemer Gambit",
|
||||
"scid": "D00l",
|
||||
"aliases": {
|
||||
"scid": "Blackmar-Diemer Gambit (BDG): 2.e4",
|
||||
"ct": "Blackmar-Diemer Gambit, General",
|
||||
"chronos": "Blackmar gambit"
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. d4 d5 2. e4 dxe4 3. Nc3 Nf6": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4 3. Nc3 Nf6",
|
||||
"name": "Blackmar-Diemer Gambit",
|
||||
"aliases": {
|
||||
"chessGraph": "Vienna Variation, BDG",
|
||||
"icsbot": "Vienna Variation, BDG "
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. d4 d5 2. e4 dxe4": [
|
||||
{
|
||||
"src": "scid",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4",
|
||||
"name": "Blackmar-Diemer Gambit (BDG): 2.e4 dxe4",
|
||||
"scid": "D00l"
|
||||
}
|
||||
],
|
||||
"1. d4 d5 2. e4 dxe4 3. Nc3 Nf6 4. f3 exf3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "D00",
|
||||
"moves": "1. d4 d5 2. e4 dxe4 3. Nc3 Nf6 4. f3 exf3",
|
||||
"name": "Blackmar-Diemer Gambit Accepted",
|
||||
"scid": "D00v",
|
||||
"aliases": {
|
||||
"scid": "Blackmar-Diemer: Accepted"
|
||||
}
|
||||
}
|
||||
],
|
||||
"1. d4 Nf6 2. c4 e6 3. g3": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3",
|
||||
"name": "Catalan Opening",
|
||||
"scid": "E00g",
|
||||
"aliases": {
|
||||
"scid": "Catalan",
|
||||
"ct": "Catalan Opening, General"
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
}
|
||||
],
|
||||
"1. d4 Nf6 2. c4 e6 3. g3 d5": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 d5",
|
||||
"name": "Catalan Opening",
|
||||
"scid": "E00n",
|
||||
"aliases": {
|
||||
"scid": "Catalan: 3...d5",
|
||||
"eco_js": "Catalan Opening, 2. c4 e6 3. g3 d5",
|
||||
"chessGraph": "E01"
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
}
|
||||
],
|
||||
"1. d4 Nf6 2. c4 e6 3. g3 e5": [
|
||||
{
|
||||
"src": "eco_tsv",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 e5",
|
||||
"name": "Catalan Opening: Hungarian Gambit",
|
||||
"scid": "E00g",
|
||||
"aliases": {
|
||||
"scid": "Catalan: Hungarian Gambit",
|
||||
"chessGraph": "Hungarian Gambit, Catalan",
|
||||
"icsbot": "Hungarian Gambit, Catalan "
|
||||
},
|
||||
"wikipediaSlug": "catalan-opening"
|
||||
}
|
||||
],
|
||||
"1. d4 Nf6 2. c4 e6 3. g3 Bb4+": [
|
||||
{
|
||||
"src": "scid",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 Bb4+",
|
||||
"name": "Catalan: 3...Bb4+",
|
||||
"scid": "E00i",
|
||||
"wikipediaSlug": "catalan"
|
||||
}
|
||||
],
|
||||
"1. d4 Nf6 2. c4 e6 3. g3 Bb4+ 4. Bd2": [
|
||||
{
|
||||
"src": "scid",
|
||||
"eco": "E00",
|
||||
"moves": "1. d4 Nf6 2. c4 e6 3. g3 Bb4+ 4. Bd2",
|
||||
"name": "Catalan: 3...Bb4+ 4.Bd2",
|
||||
"scid": "E00j",
|
||||
"wikipediaSlug": "catalan"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Game Analysis Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
// Mock the LLM API
|
||||
await page.route('**/api/v1/llm/chat', async (route) => {
|
||||
const request = route.request();
|
||||
const postData = request.postDataJSON();
|
||||
|
||||
let reply = "Let me analyze this position for you.";
|
||||
|
||||
if (postData.message?.includes('ANALYSIS MODE')) {
|
||||
reply = "I'll help you understand this game. Let's go through it move by move.";
|
||||
} else if (postData.message?.includes('Move')) {
|
||||
reply = "This is an interesting move. Let me explain the key ideas.";
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ reply }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to analysis mode', async ({ page }) => {
|
||||
// Navigate directly to analysis page
|
||||
await page.goto('/analysis');
|
||||
|
||||
// Should be on analysis page
|
||||
await expect(page).toHaveURL('/analysis');
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
// Look for the textarea which is always visible in analysis mode
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should analyze a PGN game', async ({ page }) => {
|
||||
await page.goto('/analysis');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see PGN input
|
||||
const pgnInput = page.locator('textarea').first();
|
||||
await expect(pgnInput).toBeVisible();
|
||||
|
||||
// Enter a simple PGN (just moves)
|
||||
const scholarsPgn = `1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6`;
|
||||
|
||||
await pgnInput.fill(scholarsPgn);
|
||||
|
||||
// Wait for the game to load automatically
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should see navigation controls
|
||||
const nextButton = page.locator('button').filter({ has: page.locator('svg') }).filter({ hasText: '' });
|
||||
// Just verify the textarea is still visible after loading
|
||||
await expect(page.locator('textarea').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should analyze a FEN position', async ({ page }) => {
|
||||
await page.goto('/analysis');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
const fenInput = page.locator('textarea').first();
|
||||
|
||||
// Enter a FEN position
|
||||
await fenInput.fill('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1');
|
||||
|
||||
// Wait for position to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test('should navigate through moves in analysis', async ({ page }) => {
|
||||
await page.goto('/analysis');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Enter a PGN with multiple moves
|
||||
const pgnInput = page.locator('textarea').first();
|
||||
await pgnInput.fill('1. e4 e5 2. Nf3 Nc6 3. Bb5');
|
||||
|
||||
// Wait for game to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Look for navigation buttons (they use icons, not text)
|
||||
const buttons = page.locator('button').filter({ has: page.locator('svg') });
|
||||
// Just verify we have navigation buttons
|
||||
await expect(buttons.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display analysis interface', async ({ page }) => {
|
||||
await page.goto('/analysis');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see input area
|
||||
await expect(page.locator('textarea').first()).toBeVisible();
|
||||
|
||||
// Should see evaluation bar (or check for evaluation-related elements)
|
||||
const evaluationElement = page.locator('text=Evaluation').or(page.locator('[class*="evaluation"]'));
|
||||
if (await evaluationElement.count() > 0) {
|
||||
await expect(evaluationElement.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow switching board orientation in analysis', async ({ page }) => {
|
||||
await page.goto('/analysis');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Look for orientation buttons (White/Black)
|
||||
const whiteButton = page.locator('button').filter({ hasText: /white/i });
|
||||
const blackButton = page.locator('button').filter({ hasText: /black/i });
|
||||
|
||||
// At least one orientation button should be visible
|
||||
const orientationButtons = page.locator('button').filter({ hasText: /white|black/i });
|
||||
if (await orientationButtons.count() > 0) {
|
||||
await expect(orientationButtons.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Game Playing Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
// Mock the LLM API to avoid needing a real API key
|
||||
await page.route('**/api/v1/llm/chat', async (route) => {
|
||||
const request = route.request();
|
||||
const postData = request.postDataJSON();
|
||||
|
||||
// Generate contextual responses based on the message
|
||||
let reply = "Let's play chess! Make your move.";
|
||||
|
||||
if (postData.message?.includes('NEW GAME')) {
|
||||
reply = "Welcome! I'm ready to help you improve your chess. Let's begin!";
|
||||
} else if (postData.message?.includes('PLAYER MOVE')) {
|
||||
reply = "Interesting move! Let me think about my response.";
|
||||
} else if (postData.message?.includes('COMPUTER MOVE')) {
|
||||
reply = "I've made my move. What do you think about the position?";
|
||||
} else if (postData.message?.includes('GAME OVER')) {
|
||||
reply = "Good game! Let's review what we learned.";
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ reply }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should start a new game as white', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see the start screen heading
|
||||
await expect(page.getByRole('heading', { name: 'Chess Tutor AI' }).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click "Start New Game" button
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await expect(startButton).toBeVisible();
|
||||
await startButton.click();
|
||||
|
||||
// Should see personality selection
|
||||
await expect(page.locator('button:has-text("Professional Coach")')).toBeVisible();
|
||||
|
||||
// White should be selected by default
|
||||
await expect(page.locator('button:has-text("Play as White")')).toHaveClass(/border-blue-600/);
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
// Wait for game to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
// Look for the "Undo Last Move" button which appears when the game is loaded
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see evaluation bar (if present)
|
||||
const evaluationElement = page.locator('text=Evaluation').or(page.locator('[class*="evaluation"]'));
|
||||
if (await evaluationElement.count() > 0) {
|
||||
await expect(evaluationElement.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Should see chat interface
|
||||
await expect(page.locator('input[placeholder*="Ask"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should start a new game as black', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
|
||||
// Select black
|
||||
await page.locator('button:has-text("Play as Black")').click();
|
||||
await expect(page.locator('button:has-text("Play as Black")')).toHaveClass(/border-blue-600/);
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Board should be flipped (black at bottom)
|
||||
// The computer should make the first move since we're playing as black
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test('should import a FEN position', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
|
||||
// Expand advanced options
|
||||
const advancedButton = page.locator('button:has-text("Advanced")');
|
||||
await advancedButton.click();
|
||||
|
||||
// Enter a FEN position (Scholar's Mate position)
|
||||
const fenInput = page.locator('textarea[placeholder*="Paste FEN"]');
|
||||
await expect(fenInput).toBeVisible();
|
||||
await fenInput.fill('r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4');
|
||||
|
||||
// Should detect FEN format
|
||||
await expect(page.locator('text=FEN Position')).toBeVisible();
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should load the position (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test('should show game controls', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Start a quick game
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Wait for game to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see game control buttons (using more flexible selectors)
|
||||
await expect(page.locator('button').filter({ hasText: /Resign|Give up/i }).first()).toBeVisible();
|
||||
await expect(page.locator('button').filter({ hasText: /Download|Save/i }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Global setup for Playwright e2e tests
|
||||
* Copies test fixtures to public directory before tests run
|
||||
*/
|
||||
export default function globalSetup() {
|
||||
console.log('🔧 Setting up e2e test environment...');
|
||||
|
||||
const fixturesDir = path.join(__dirname, 'fixtures/openings');
|
||||
const targetDir = path.join(__dirname, '../public/openings');
|
||||
|
||||
// Ensure target directory exists
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy fixture files
|
||||
const files = ['ecoA.json', 'ecoB.json', 'ecoC.json', 'ecoD.json', 'ecoE.json', 'moveIndex.json'];
|
||||
|
||||
for (const file of files) {
|
||||
const source = path.join(fixturesDir, file);
|
||||
const target = path.join(targetDir, file);
|
||||
|
||||
if (fs.existsSync(source)) {
|
||||
fs.copyFileSync(source, target);
|
||||
console.log(` ✓ Copied ${file}`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Fixture not found: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Test fixtures ready\n');
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Opening Training - Interactive Move Tests
|
||||
*
|
||||
* These tests verify the core opening training functionality including:
|
||||
* - User makes correct moves in theory
|
||||
* - User makes incorrect moves
|
||||
* - Navigation back and forth through move history
|
||||
* - Opponent automatically responds in theory
|
||||
* - Correcting mistakes after navigating back
|
||||
*/
|
||||
|
||||
test.describe('Opening Training - Interactive Scenarios', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
await page.goto('/learning/openings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should handle user playing as White - correct moves in theory', async ({ page }) => {
|
||||
// Select an opening where user plays White (e.g., C00 - French Defense)
|
||||
// In French Defense, White plays first with 1.e4
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Verify board is visible
|
||||
await expect(page.locator('[aria-label*="Chess board"]')).toBeVisible();
|
||||
|
||||
// Verify it's White's turn (user's turn)
|
||||
// The board should show the starting position
|
||||
await expect(page.getByText('No moves yet. Make your first move!')).toBeVisible();
|
||||
|
||||
// Make the first move: e2 to e4 (drag and drop)
|
||||
const board = page.locator('[aria-label*="Chess board"]').first();
|
||||
await expect(board).toBeVisible();
|
||||
|
||||
// Wait a bit for Stockfish to be ready
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// For now, we'll log this test as a placeholder since actual drag-drop
|
||||
// requires more complex interaction with react-chessboard
|
||||
console.log('[Test] Would make move e2-e4 here');
|
||||
|
||||
// TODO: Implement actual drag-drop interaction
|
||||
// This requires:
|
||||
// 1. Finding the e2 square element
|
||||
// 2. Finding the e4 square element
|
||||
// 3. Performing drag and drop
|
||||
// 4. Waiting for move to be processed
|
||||
});
|
||||
|
||||
test('should handle user playing as Black - opponent moves first', async ({ page }) => {
|
||||
// Select an opening where user plays Black
|
||||
// For example, a 1.e4 opening from Black's perspective
|
||||
await page.goto('/learning/openings/C00'); // French Defense from Black's perspective
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// In this case, we expect the opponent (White) to make the first move automatically
|
||||
// We should see that White has moved e2-e4
|
||||
|
||||
// Wait for auto-move to happen
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check if move history shows a move
|
||||
const moveHistory = page.locator('[aria-label="List of moves played"]');
|
||||
await expect(moveHistory).toBeVisible();
|
||||
|
||||
// If the opening is from Black's perspective, White should move first
|
||||
// We should see "1. e4" or similar in the move list
|
||||
|
||||
console.log('[Test] Would verify opponent moved first');
|
||||
});
|
||||
|
||||
test('should handle navigation: make wrong move, go back, make correct move', async ({ page }) => {
|
||||
// This is the key scenario the user reported
|
||||
// 1. User makes a wrong move
|
||||
// 2. System identifies it as wrong
|
||||
// 3. User clicks "Back" button to navigate back
|
||||
// 4. User makes the correct move
|
||||
// 5. Opponent should respond (this was the bug)
|
||||
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('[Test] Scenario:');
|
||||
console.log('[Test] 1. User would make a WRONG move');
|
||||
console.log('[Test] 2. System would categorize it as "weak" or "playable" not "in-theory"');
|
||||
console.log('[Test] 3. User clicks BACK button');
|
||||
console.log('[Test] 4. User makes CORRECT move');
|
||||
console.log('[Test] 5. Opponent should respond');
|
||||
|
||||
// Verify navigation buttons exist
|
||||
const backButton = page.locator('button[aria-label="Go to previous move"]');
|
||||
const forwardButton = page.locator('button[aria-label="Go to next move"]');
|
||||
|
||||
await expect(backButton).toBeVisible();
|
||||
await expect(forwardButton).toBeVisible();
|
||||
|
||||
// Verify back button is disabled initially (no moves yet)
|
||||
await expect(backButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should show move classification in history', async ({ page }) => {
|
||||
// After making moves, each move should show classification
|
||||
// - in-theory (green)
|
||||
// - playable (yellow)
|
||||
// - weak (red)
|
||||
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check that move history section is ready
|
||||
await expect(page.getByText('Move History')).toBeVisible();
|
||||
|
||||
console.log('[Test] After moves are made, would verify:');
|
||||
console.log('[Test] - Moves show colored badges (green/yellow/red)');
|
||||
console.log('[Test] - Move classification text is visible');
|
||||
console.log('[Test] - Can click on moves to navigate');
|
||||
});
|
||||
|
||||
test('should clear future moves when making new move after navigation back', async ({ page }) => {
|
||||
// This test verifies that when a user:
|
||||
// 1. Makes moves A, B, C
|
||||
// 2. Navigates back to after move A
|
||||
// 3. Makes a different move D
|
||||
// Then moves B and C should be discarded
|
||||
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('[Test] Scenario:');
|
||||
console.log('[Test] 1. Make moves: e4, e5, Nf3 (for example)');
|
||||
console.log('[Test] 2. Navigate back to position after e4');
|
||||
console.log('[Test] 3. Make different move: d4 instead of e5');
|
||||
console.log('[Test] 4. Verify e5 and Nf3 are removed from history');
|
||||
console.log('[Test] 5. Verify new branch starts from e4, d4');
|
||||
});
|
||||
|
||||
test('should resume session and continue if opponent turn', async ({ page }) => {
|
||||
// Create a session, make some moves, leave, come back
|
||||
// If it's opponent's turn when resuming, they should move
|
||||
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Make initial session with some moves
|
||||
console.log('[Test] Would make moves to create a session');
|
||||
|
||||
// Navigate away
|
||||
await page.goto('/learning');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Navigate back to the opening
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// User might see "Resume Session" dialog
|
||||
const resumeButton = page.locator('button:has-text("Resume Session")');
|
||||
const startFreshButton = page.locator('button:has-text("Start Fresh")');
|
||||
|
||||
// If dialog appears, click Resume
|
||||
if (await resumeButton.isVisible({ timeout: 2000 })) {
|
||||
await resumeButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
console.log('[Test] After resuming:');
|
||||
console.log('[Test] - If opponent turn, they should move automatically');
|
||||
console.log('[Test] - If user turn, board should wait for user');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper test for debugging chess board interaction
|
||||
*/
|
||||
test.describe('Opening Training - Board Interaction Debugging', () => {
|
||||
test.skip('debug: inspect chess board structure', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
await page.goto('/learning/openings/C00');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Take a screenshot to see the board
|
||||
await page.screenshot({ path: 'e2e/debug-screenshots/chessboard.png', fullPage: true });
|
||||
|
||||
// Try to find pieces and squares
|
||||
const boardElement = page.locator('[aria-label*="Chess board"]').first();
|
||||
await expect(boardElement).toBeVisible();
|
||||
|
||||
// Log the board HTML structure for debugging
|
||||
const boardHTML = await boardElement.innerHTML();
|
||||
console.log('[Debug] Board HTML structure:');
|
||||
console.log(boardHTML.substring(0, 500)); // First 500 chars
|
||||
|
||||
// Try to find SVG pieces
|
||||
const pieces = page.locator('svg image[href*="piece"]');
|
||||
const pieceCount = await pieces.count();
|
||||
console.log(`[Debug] Found ${pieceCount} piece elements`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E Tests for Opening Training Feature
|
||||
*
|
||||
* These tests verify the complete training flow from opening selection
|
||||
* to receiving move feedback with engine evaluation and LLM explanations.
|
||||
*/
|
||||
|
||||
test.describe('Opening Training', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
// Navigate to opening selection page
|
||||
await page.goto('/learning/openings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should display opening selection page with family groups', async ({ page }) => {
|
||||
// Check page title
|
||||
await expect(page.getByText('Opening Training')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Check that opening family cards are present (look for the family buttons instead of category headers)
|
||||
const familyCards = page.locator('button[aria-label*="opening family"]');
|
||||
await expect(familyCards.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should have multiple families
|
||||
const count = await familyCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should display opening families by category', async ({ page }) => {
|
||||
// Check that family cards are displayed (wait for them to load)
|
||||
const familyCards = page.locator('button[aria-label*="opening family"]');
|
||||
|
||||
// Wait for at least one family card to be visible
|
||||
await expect(familyCards.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Give a moment for other cards to render
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should have at least one family displayed (could be more depending on data)
|
||||
const count = await familyCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should select an opening family', async ({ page }) => {
|
||||
// Look for a family card (e.g., one with "Variations:" text)
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Get the family name before clicking
|
||||
const familyName = await familyCard.locator('h3').textContent();
|
||||
|
||||
// Click on the family
|
||||
await familyCard.click();
|
||||
|
||||
// Should see the family name in the selector or openings list
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
test('should navigate directly to an opening', async ({ page }) => {
|
||||
// Test direct navigation first
|
||||
await page.goto('/learning/openings/A01');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Current URL:', page.url());
|
||||
|
||||
// Check if we're on the right page
|
||||
const pageContent = await page.textContent('body');
|
||||
console.log('Page contains Opening Training:', pageContent?.includes('Opening Training'));
|
||||
console.log('Page contains Nimzo-Larsen:', pageContent?.includes('Nimzo-Larsen'));
|
||||
|
||||
// Verify chess board or loading state
|
||||
const hasBoard = await page.locator('[aria-label*="Chess board"]').count();
|
||||
const hasLoading = await page.locator('text="Initializing training session"').count();
|
||||
const hasNoSession = await page.locator('text="No active session"').count();
|
||||
|
||||
console.log('Has chessboard:', hasBoard);
|
||||
console.log('Has loading:', hasLoading);
|
||||
console.log('Has no session:', hasNoSession);
|
||||
});
|
||||
|
||||
test('should start a training session for an opening', async ({ page }) => {
|
||||
// Enable console logging
|
||||
const consoleLogs: Array<{ type: string; text: string }> = [];
|
||||
page.on('console', msg => {
|
||||
consoleLogs.push({ type: msg.type(), text: msg.text() });
|
||||
console.log(`[Browser ${msg.type()}]:`, msg.text());
|
||||
});
|
||||
|
||||
// Track page errors
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
pageErrors.push(error.message);
|
||||
console.error('Page Error:', error.message);
|
||||
});
|
||||
|
||||
// First, select a family to show opening cards
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Now get opening cards using data-testid
|
||||
const openingCards = page.locator('[data-testid^="opening-card-"]');
|
||||
const count = await openingCards.count();
|
||||
console.log('Found opening cards:', count);
|
||||
|
||||
// Get first opening card
|
||||
const firstOpening = openingCards.first();
|
||||
const eco = await firstOpening.getAttribute('data-opening-eco');
|
||||
const openingText = await firstOpening.textContent();
|
||||
console.log('Clicking on opening:', eco, openingText?.substring(0, 30));
|
||||
|
||||
const currentUrl = page.url();
|
||||
console.log('Current URL before click:', currentUrl);
|
||||
|
||||
await firstOpening.click();
|
||||
|
||||
// Wait a bit for navigation to start
|
||||
await page.waitForTimeout(500);
|
||||
console.log('URL after click:', page.url());
|
||||
|
||||
// Wait for training page to load
|
||||
try {
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/, { timeout: 10000 });
|
||||
console.log('Successfully navigated to:', page.url());
|
||||
} catch (e) {
|
||||
console.error('Failed to navigate. Current URL:', page.url());
|
||||
console.error('Console logs:', consoleLogs.slice(-10)); // Last 10 logs
|
||||
console.error('Page errors:', pageErrors);
|
||||
|
||||
// Take screenshot for debugging
|
||||
await page.screenshot({ path: 'e2e/screenshots/navigation-failed.png', fullPage: true });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Wait for the page to settle
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Log any errors that occurred
|
||||
if (pageErrors.length > 0) {
|
||||
console.error('Page errors found:', pageErrors);
|
||||
await page.screenshot({ path: 'e2e/screenshots/page-errors.png', fullPage: true });
|
||||
}
|
||||
|
||||
// Verify chess board is visible
|
||||
await expect(page.locator('[aria-label*="Chess board"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify move history section
|
||||
await expect(page.getByText('Move History')).toBeVisible();
|
||||
|
||||
// Verify Wikipedia summary loads (check for any of the possible states)
|
||||
const wikiSection = page.locator('text=/Loading opening background|No background information|Wikipedia/i');
|
||||
await expect(wikiSection.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Ensure no critical errors occurred
|
||||
expect(pageErrors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should show session recovery dialog for existing session', async ({
|
||||
page,
|
||||
}) => {
|
||||
// First, select a family to show opening cards
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Start a session
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
// Wait for session to initialize
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate away and back
|
||||
await page.goBack();
|
||||
|
||||
// Select family again
|
||||
const familyCardAgain = page.locator('button[aria-label*="opening family"]').first();
|
||||
await familyCardAgain.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const firstOpeningAgain = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpeningAgain.click();
|
||||
|
||||
// Expect recovery dialog if session has moves
|
||||
// (This would need actual moves to be made in a more complete test)
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
});
|
||||
|
||||
test.skip('should make a move and receive feedback', async ({ page }) => {
|
||||
// NOTE: This test is skipped because it requires:
|
||||
// 1. Stockfish engine to be loaded
|
||||
// 2. Move interaction with the chess board
|
||||
// 3. LLM API to be available
|
||||
//
|
||||
// To implement:
|
||||
// 1. Start a training session
|
||||
// 2. Make a move on the board (drag and drop)
|
||||
// 3. Wait for engine evaluation
|
||||
// 4. Verify move feedback is displayed
|
||||
// 5. Verify categorization (in-theory/playable/weak)
|
||||
// 6. Verify LLM explanation appears
|
||||
});
|
||||
|
||||
test.skip('should navigate through move history', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires moves to be made first
|
||||
//
|
||||
// To implement:
|
||||
// 1. Make several moves
|
||||
// 2. Click "Back" button
|
||||
// 3. Verify board position changes
|
||||
// 4. Verify feedback updates
|
||||
// 5. Click "Forward" button
|
||||
// 6. Verify board returns to later position
|
||||
});
|
||||
|
||||
test.skip('should detect deviation from theory', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires specific opening and off-book move
|
||||
//
|
||||
// To implement:
|
||||
// 1. Start a known opening
|
||||
// 2. Make moves following theory
|
||||
// 3. Make an off-book move
|
||||
// 4. Verify "Off-book since move X" indicator appears
|
||||
// 5. Verify feedback categorizes move as playable or weak (not in-theory)
|
||||
});
|
||||
|
||||
test.skip('should cache LLM explanations on navigation', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires checking network requests
|
||||
//
|
||||
// To implement:
|
||||
// 1. Make a move and wait for LLM explanation
|
||||
// 2. Navigate to different move
|
||||
// 3. Navigate back to original move
|
||||
// 4. Verify no new LLM API call was made (check network tab)
|
||||
// 5. Verify explanation is displayed immediately
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Performance Tests
|
||||
*/
|
||||
test.describe('Opening Training Performance', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
});
|
||||
|
||||
test.skip('should initialize session within 3 seconds', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/learning/openings');
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
await page.waitForSelector('[aria-label*="Chess board"]');
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test.skip('should evaluate position within 2 seconds', async ({ page }) => {
|
||||
// NOTE: Requires making a move and measuring eval time
|
||||
// Target: Engine evaluation completes in <2s for 95% of moves
|
||||
});
|
||||
|
||||
test.skip('should provide complete feedback within 5 seconds', async ({
|
||||
page,
|
||||
}) => {
|
||||
// NOTE: Requires making a move and measuring total feedback time
|
||||
// Target: Engine eval + LLM explanation in <5s for 90% of moves
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Accessibility Tests
|
||||
*/
|
||||
test.describe('Opening Training Accessibility', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
});
|
||||
|
||||
test('should have proper ARIA labels on key elements', async ({ page }) => {
|
||||
await page.goto('/learning/openings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Select a family first
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click on first opening
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
|
||||
// Verify ARIA labels
|
||||
await expect(page.locator('[aria-label*="Chess board"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Move history and navigation"]')
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Go to previous move"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Go to next move"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be keyboard navigable', async ({ page }) => {
|
||||
await page.goto('/learning/openings');
|
||||
|
||||
// Tab through elements
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Verify focus is visible
|
||||
const focused = await page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focused).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
@@ -0,0 +1,170 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Settings and UI Features', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to settings page', async ({ page }) => {
|
||||
// Navigate directly to settings page
|
||||
await page.goto('/settings');
|
||||
|
||||
// Should be on settings page
|
||||
await expect(page).toHaveURL('/settings');
|
||||
|
||||
// Should see settings heading
|
||||
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display language selector', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see language options (buttons show language codes like EN, DE, FR)
|
||||
await expect(page.getByRole('button', { name: 'EN', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'DE', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'FR', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'IT', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'PL', exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should switch language', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click German language
|
||||
const deutschButton = page.getByRole('button', { name: 'DE', exact: true });
|
||||
await deutschButton.click();
|
||||
|
||||
// Wait for language to change
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// DE button should now be selected (blue background)
|
||||
await expect(deutschButton).toHaveClass(/bg-blue-600/);
|
||||
});
|
||||
|
||||
test('should display API key management', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see API key section
|
||||
await expect(page.locator('text=Google Gemini API Key')).toBeVisible();
|
||||
|
||||
// Should see API key input
|
||||
const apiKeyInput = page.locator('input[type="password"]').or(page.locator('input[placeholder*="API"]'));
|
||||
await expect(apiKeyInput.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should save API key', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Enter API key
|
||||
const apiKeyInput = page.locator('input[type="password"]').or(page.locator('input[placeholder*="API"]'));
|
||||
await apiKeyInput.first().fill('test-api-key-12345');
|
||||
|
||||
// Should see save button
|
||||
const saveButton = page.locator('button:has-text("Save")').or(page.locator('button:has-text("Update")'));
|
||||
if (await saveButton.count() > 0) {
|
||||
await saveButton.first().click();
|
||||
|
||||
// Should see success message or confirmation
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Verify API key is saved in localStorage
|
||||
const savedKey = await page.evaluate(() => localStorage.getItem('gemini_api_key'));
|
||||
expect(savedKey).toBe('test-api-key-12345');
|
||||
});
|
||||
|
||||
test('should display save button', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see save button
|
||||
await expect(page.locator('button:has-text("Save")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate back from settings', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click back arrow button
|
||||
const backButton = page.locator('button').filter({ has: page.locator('svg') }).first();
|
||||
await backButton.click();
|
||||
|
||||
// Should return to onboarding or home page
|
||||
await page.waitForTimeout(500);
|
||||
// URL should change from /settings
|
||||
expect(page.url()).not.toContain('/settings');
|
||||
});
|
||||
|
||||
test('should display header on all pages', async ({ page }) => {
|
||||
// Check home page
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const homeHeader = page.getByRole('heading', { name: 'Chess Tutor AI' });
|
||||
if (await homeHeader.count() > 0) {
|
||||
await expect(homeHeader.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Check learning page
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const learningHeader = page.getByRole('heading', { name: 'Chess Tutor AI' });
|
||||
if (await learningHeader.count() > 0) {
|
||||
await expect(learningHeader.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Check settings page
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const settingsHeader = page.getByRole('heading', { name: 'Chess Tutor AI' });
|
||||
if (await settingsHeader.count() > 0) {
|
||||
await expect(settingsHeader.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show personality options on start screen', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click Start New Game
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
|
||||
// Should see personality options (check for at least 3 of the available personalities)
|
||||
await expect(page.locator('button:has-text("Professional Coach")').first()).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Friendly Motivator")').first()).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Opening Professor")').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display start screen when API key exists', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see the start screen heading
|
||||
await expect(page.getByRole('heading', { name: 'Chess Tutor AI' }).first()).toBeVisible();
|
||||
|
||||
// Should see main action buttons
|
||||
await expect(page.locator('button:has-text("Start New Game")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should redirect to onboarding when API key is missing', async ({ browser }) => {
|
||||
// Create a new context without the API key
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should redirect to onboarding
|
||||
await expect(page).toHaveURL('/onboarding');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Tactical Practice Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to learning area from start screen', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the Learning Area button to be visible
|
||||
const learningButton = page.locator('button:has-text("Learning Area")').last();
|
||||
await expect(learningButton).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click on Learning Area button
|
||||
await learningButton.click();
|
||||
|
||||
// Should be on the learning page
|
||||
await expect(page).toHaveURL('/learning');
|
||||
|
||||
// Should see tactical pattern buttons
|
||||
await expect(page.locator('text=PIN')).toBeVisible();
|
||||
await expect(page.locator('text=FORK')).toBeVisible();
|
||||
await expect(page.locator('text=SKEWER')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should select a coach before starting practice', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see personality selection
|
||||
await expect(page.locator('button:has-text("Professional Coach")').first()).toBeVisible();
|
||||
|
||||
// Select a coach
|
||||
const professionalCoach = page.locator('button:has-text("Professional Coach")').first();
|
||||
await professionalCoach.click();
|
||||
|
||||
// Coach should be selected (button should have blue border)
|
||||
await expect(professionalCoach).toHaveClass(/border-blue-500/);
|
||||
});
|
||||
|
||||
test('should start PIN tactical practice', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Select a coach first
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
// Click on PIN button
|
||||
await page.click('text=PIN');
|
||||
|
||||
// Should navigate to PIN practice page
|
||||
await expect(page).toHaveURL('/learning/tactics/pin');
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
// Look for statistics which are always visible
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Should see statistics display
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Incorrect', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Current Streak', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Best Streak', { exact: true })).toBeVisible();
|
||||
|
||||
// Should see difficulty selector
|
||||
await expect(page.locator('button:has-text("Easy")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Medium")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Hard")')).toBeVisible();
|
||||
|
||||
// Should see puzzle rating
|
||||
await expect(page.locator('text=Puzzle Rating')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display skip button', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see Skip Puzzle button
|
||||
await expect(page.locator('button:has-text("Skip Puzzle")')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should switch difficulty levels', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for initial puzzle to load
|
||||
await page.waitForSelector('text=Puzzle Rating', { timeout: 10000 });
|
||||
|
||||
// Close any modals or overlays that might be blocking
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Get initial difficulty button state (Easy is selected by default with green background)
|
||||
const easyButton = page.locator('button:has-text("Easy")');
|
||||
await expect(easyButton).toHaveClass(/bg-green-500/); // Easy is green when selected
|
||||
|
||||
// Click Medium difficulty - use force to bypass any overlays
|
||||
await page.locator('button:has-text("Medium")').click({ force: true });
|
||||
|
||||
// Wait for new puzzle to load
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Medium button should now be selected (yellow background)
|
||||
const mediumButton = page.locator('button:has-text("Medium")');
|
||||
await expect(mediumButton).toHaveClass(/bg-yellow-500/);
|
||||
});
|
||||
|
||||
test('should show coach chat interface', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=FORK');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Wait for page to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Close any modals or overlays that might be open
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Should see input field for asking questions
|
||||
await expect(page.locator('input[placeholder*="Ask"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should see send button (it's an icon-only button with the Send icon)
|
||||
const sendButton = page.locator('button[type="submit"]').or(page.locator('form button').last());
|
||||
await expect(sendButton.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should update statistics when skipping puzzle', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Wait for puzzle to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Close any modals or overlays that might be blocking
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Find the incorrect stat card - it's the second stat card
|
||||
const statCards = page.locator('.bg-white.dark\\:bg-gray-800.rounded-lg.p-4');
|
||||
const incorrectCard = statCards.nth(1);
|
||||
const incorrectValue = incorrectCard.locator('.text-2xl.font-bold');
|
||||
|
||||
// Initial stats should be 0
|
||||
await expect(incorrectValue).toHaveText('0');
|
||||
|
||||
// Click Skip Puzzle - use force if needed to bypass overlays
|
||||
await page.locator('button:has-text("Skip Puzzle")').click({ force: true });
|
||||
|
||||
// Wait for stats to update and new puzzle to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Incorrect count should increase
|
||||
await expect(incorrectValue).toHaveText('1');
|
||||
});
|
||||
|
||||
test('should navigate back to learning area', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click back button
|
||||
await page.click('text=Back to Learning');
|
||||
|
||||
// Should be back on learning page
|
||||
await expect(page).toHaveURL('/learning');
|
||||
});
|
||||
|
||||
test('should display all 8 tactical patterns', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
|
||||
const patterns = [
|
||||
'PIN',
|
||||
'SKEWER',
|
||||
'FORK',
|
||||
'DISCOVERED CHECK',
|
||||
'DOUBLE ATTACK',
|
||||
'OVERLOADING',
|
||||
'BACK RANK WEAKNESS',
|
||||
'TRAPPED PIECE'
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
await expect(page.locator(`text=${pattern}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user