#include "strans.c" #include "test.h" static int typekeys(struct ct*, char*, Str*); static int draindraw(Drawcmd*); /* One key from the owner, as imhandlekey drives the engine. */ static int keystroke(u32int ks, u32int mod, Str *com) { int eaten; eaten = transition(ks, mod, com); redraw(); return eaten; } void testengineinit(int lang) { init(); im.l = getlang(lang); } void testenginehandle(Keyreq *req) { imhandlekey(req); } void* testengineowner(void) { return activeowner; } void testenginepreedit(Str *preedit) { impre(&im, preedit); } void testenginecaret(Caret *at) { *at = caret; } void vietnamese_state_lifetime(struct ct *t) { Str com; init(); im.l = &testvi; im.pre = mkstr("as"); im.raw = mkstr("as"); sclear(&com); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "committed Telex", "á", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); init(); im.l = &testvi; im.pre = mkstr("as"); im.raw = mkstr("as"); sclear(&com); CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_EQ_INT(t, 0, com.n); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); init(); im.l = &testvi; im.pre = mkstr("as"); im.raw = mkstr("as"); sclear(&com); CT_CHECK(t, dotrans('q', &com)); checkstr(t, "completed Telex", "á", &com); checkstr(t, "next preedit", "q", &im.pre); checkstr(t, "next raw input", "q", &im.raw); } void engine_clears_candidates(struct ct *t) { Str com; init(); im.l = getlang(LangKO); im.pre = mkstr("ㄱ"); im.nkouho = 2; im.sel = 1; sclear(&com); dotrans('k', &com); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); } void engine_backspace_clears_candidates(struct ct *t) { Lang *lang; Str com, shown; lang = getlang(LangJP); init(); im.l = lang; sclear(&com); CT_CHECK(t, keystroke('n', 0, &com)); CT_CHECK(t, keystroke('a', 0, &com)); impre(&im, &shown); checkstr(t, "before backspace", "な", &shown); im.kouho[0] = mkstr("stale-candidate"); im.nkouho = 1; im.sel = 0; CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "backspace commit", "", &com); checkstr(t, "pending romaji after backspace", "n", &im.raw); impre(&im, &shown); checkstr(t, "shown preedit after backspace", "ん", &shown); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); } void engine_selects_visible_candidate(struct ct *t) { static const struct { int n, sel; Rune key; char *want; } cases[] = { { Maxdisp, 0, '9', "c9" }, { Maxkouho, Maxdisp, '1', "c10" }, { Maxkouho, 2*Maxdisp, '9', "c27" }, }; char name[8]; Str com; int i, j; for(i = 0; i < nelem(cases); i++){ init(); for(j = 0; j < cases[i].n; j++){ snprint(name, sizeof name, "c%d", j+1); im.kouho[j] = mkstr(name); } im.nkouho = cases[i].n; im.sel = cases[i].sel; sclear(&com); CT_CHECK(t, keystroke(cases[i].key, 0, &com)); checkstr(t, cases[i].want, cases[i].want, &com); CT_EQ_INT(t, 0, im.nkouho); } } void engine_candidate_shortcut_modifiers(struct ct *t) { static u32int mods[] = { Mshift, Mctrl, Malt, Msuper }; Str com, selected; int i; for(i = 0; i < nelem(mods); i++){ init(); im.kouho[9] = mkstr("must-not-select"); im.nkouho = 10; im.sel = 9; selected = im.kouho[9]; sclear(&com); CT_CHECK(t, !keystroke('1', mods[i], &com)); CT_CHECK(t, scmp(&com, &selected) != 0); } } static Keyres ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod, Caret *caret) { Channel *reply; Keyreq req; Keyres res; reply = chancreate(sizeof(Keyres), 1); memset(&req, 0, sizeof req); req.owner = owner; req.cap = cap; req.op = op; req.ks = key; req.mod = mod; if(caret != nil) req.caret = *caret; req.reply = reply; imhandlekey(&req); chanrecv(reply, &res); chanfree(reply); return res; } static Keyres ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret) { return ownerrequestatcap(owner, 0, op, key, mod, caret); } static Keyres ownerrequest(void *owner, int op, Rune key, u32int mod) { return ownerrequestat(owner, op, key, mod, nil); } static Keyres ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod) { return ownerrequestatcap(owner, cap, op, key, mod, nil); } void engine_active_owner_lifecycle(struct ct *t) { Keyres res; Str shown; char a, b, c; init(); im.l = getlang(LangJP); res = ownerrequest(&a, Keypress, 'k', 0); CT_CHECK(t, res.eaten); res = ownerrequest(&a, Keypress, 'a', 0); checkstr(t, "first owner preedit", "か", &res.preedit); CT_EQ_PTR(t, &a, activeowner); res = ownerrequest(&b, Keypress, 'n', 0); checkstr(t, "takeover resets old preedit", "ん", &res.preedit); CT_EQ_PTR(t, &b, activeowner); res = ownerrequest(&a, Keyreset, 0, 0); CT_EQ_INT(t, 0, res.preedit.n); impre(&im, &shown); checkstr(t, "stale reset preserves owner", "ん", &shown); CT_EQ_PTR(t, &b, activeowner); res = ownerrequest(&b, Keypress, 'y', 0); res = ownerrequest(&b, Keypress, 'a', 0); checkstr(t, "current owner continues", "にゃ", &res.preedit); ownerrequest(&a, Keyrelease, 0, 0); impre(&im, &shown); checkstr(t, "stale release preserves owner", "にゃ", &shown); CT_EQ_PTR(t, &b, activeowner); ownerrequest(&b, Keyrelease, 0, 0); CT_EQ_PTR(t, nil, activeowner); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); ownerrequest(&c, Keypress, Kmodfirst, 0); CT_EQ_PTR(t, nil, activeowner); res = ownerrequest(&c, Keypress, 'k', 0); CT_EQ_PTR(t, &c, activeowner); checkstr(t, "next owner first key", "k", &res.preedit); } void engine_active_owner_reset(struct ct *t) { Caret at; Keyres res; char owner; memset(&at, 0, sizeof at); at.valid = 1; at.x = 10; at.y = 20; at.h = 14; init(); im.l = getlang(LangJP); ownerrequestat(&owner, Keypress, 'k', 0, &at); res = ownerrequest(&owner, Keypress, 'a', 0); checkstr(t, "preedit before reset", "か", &res.preedit); res = ownerrequest(&owner, Keyreset, 0, 0); CT_EQ_PTR(t, &owner, activeowner); CT_EQ_INT(t, 0, res.preedit.n); CT_CHECK(t, !caret.valid); res = ownerrequest(&owner, Keypress, 'n', 0); checkstr(t, "same owner after reset", "ん", &res.preedit); } void engine_active_owner_caret(struct ct *t) { Caret a, moved; Drawcmd dc; char one, two; memset(&a, 0, sizeof a); a.valid = 1; a.x = 10; a.y = 20; a.h = 14; moved = a; moved.x = 80; init(); draindraw(nil); im.l = getlang(LangEN); ownerrequestat(&one, Keypress, 'x', 0, &a); CT_CHECK(t, caret.valid); CT_EQ_INT(t, 10, caret.x); ownerrequestat(&one, Keycaret, 0, 0, &moved); CT_EQ_INT(t, 80, caret.x); CT_EQ_INT(t, 0, draindraw(nil)); im.kouho[0] = mkstr("candidate"); im.nkouho = 1; im.sel = 0; redraw(); if(CT_CHECK(t, draindraw(&dc) > 0)) CT_EQ_INT(t, 80, dc.caret.x); moved.x = 90; ownerrequestat(&two, Keycaret, 0, 0, &moved); CT_EQ_INT(t, 80, caret.x); CT_EQ_INT(t, 0, draindraw(nil)); ownerrequestat(&one, Keycaret, 0, 0, &moved); CT_EQ_INT(t, 90, caret.x); if(CT_CHECK(t, draindraw(&dc) > 0)) CT_EQ_INT(t, 90, dc.caret.x); ownerrequestat(&one, Keycaret, 0, 0, &moved); CT_EQ_INT(t, 0, draindraw(nil)); ownerrequestat(&one, Keypress, 'x', 0, &moved); CT_EQ_INT(t, 0, draindraw(nil)); moved.x = 100; ownerrequestat(&one, Keypress, 'x', 0, &moved); CT_EQ_INT(t, 100, caret.x); if(CT_CHECK(t, draindraw(&dc) > 0)) CT_EQ_INT(t, 100, dc.caret.x); ownerrequest(&two, Keypress, 'n', 0); CT_EQ_PTR(t, &two, activeowner); CT_CHECK(t, !caret.valid); } void engine_vietnamese_client_preedit(struct ct *t) { static const struct { char *keys; char *shown; } cases[] = { { "as", "á" }, { "aa", "â" }, { "ddoong", "đông" }, { "hoaf", "hoà" }, { "quas", "quá" }, }; Drawcmd dc; Keyres res; Str com, shown; char *k, owner; int i; for(i = 0; i < nelem(cases); i++){ init(); im.l = &testvi; draindraw(nil); sclear(&com); for(k = cases[i].keys; *k != '\0'; k++){ res = ownerrequestcap(&owner, Cclientpreedit, Keypress, *k, 0); sappend(&com, &res.commit); if(!res.eaten) sputr(&com, *k); } /* What the client shows, what the popup shows, and what * Enter commits are the same composed text. */ shown = com; sappend(&shown, &res.preedit); checkstr(t, "client preedit", cases[i].shown, &shown); activecap = 0; redraw(); if(CT_CHECK(t, draindraw(&dc) > 0)) CT_EQ_INT(t, 0, scmp(&dc.pre, &res.preedit)); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "commit", cases[i].shown, &com); } } void engine_commit_contract(struct ct *t) { static const struct { Rune key; char *one; char *all; } cases[] = { { 'z', "ㅋ", "ㅋㅋㅋ" }, { 'r', "ㄱ", "ㄱㄱㄱ" }, { 'd', "ㅇ", "ㅇㅇㅇ" }, { 'k', "ㅏ", "ㅏㅏㅏ" }, }; Keyres res; Str all, com; char owner; int i, j; for(j = 0; j < nelem(cases); j++){ init(); im.l = getlang(LangKO); sclear(&all); for(i = 0; i < 3; i++){ res = ownerrequestcap(&owner, Cclientpreedit, Keypress, cases[j].key, 0); CT_CHECK(t, res.eaten); checkstr(t, "repeated jamo commit", i == 0 ? "" : cases[j].one, &res.commit); checkstr(t, "repeated jamo preedit", cases[j].one, &res.preedit); sappend(&all, &res.commit); } sappend(&all, &res.preedit); checkstr(t, "repeated jamo total", cases[j].all, &all); } init(); im.l = getlang(LangJP); sclear(&com); if(!typekeys(t, "ka", &com)) return; CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "return commit", "か", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); init(); im.l = getlang(LangJP); sclear(&com); if(!typekeys(t, "ka", &com)) return; im.kouho[0] = mkstr("c1"); im.kouho[1] = mkstr("c2"); im.nkouho = 2; im.sel = 0; CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "selected candidate", "c2", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); init(); im.l = getlang(LangJP); sclear(&com); if(!typekeys(t, "ka", &com)) return; im.nkouho = 1; im.sel = -1; CT_CHECK(t, !keystroke('q', 0, &com)); checkstr(t, "uneaten-key commit", "か", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); init(); im.l = getlang(LangJP); sclear(&com); if(!typekeys(t, "ka", &com)) return; CT_CHECK(t, keystroke(0xf008, 0, &com)); CT_EQ_INT(t, 2, com.n); CT_EQ_UINT(t, 0x304b, com.r[0]); CT_EQ_UINT(t, 0xf008, com.r[1]); CT_EQ_INT(t, 0, im.pre.n); } void engine_telex_history_bound(struct ct *t) { Str com; Rune tone; int i; init(); im.l = &testvi; sclear(&com); CT_CHECK(t, keystroke('a', 0, &com)); for(i = 0; i < Maxrunes; i++){ tone = i % 2 == 0 ? 's' : 'f'; if(!CT_CHECK(t, keystroke(tone, 0, &com))) break; } checkstr(t, "bounded Telex commit", "à", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); } void engine_korean_modifiers_and_backspace(struct ct *t) { static const struct { Rune key; char *plain; char *shifted; } cases[] = { { 'r', "ㄱ", "ㄲ" }, { 'e', "ㄷ", "ㄸ" }, { 'q', "ㅂ", "ㅃ" }, { 't', "ㅅ", "ㅆ" }, { 'w', "ㅈ", "ㅉ" }, { 'o', "ㅐ", "ㅒ" }, { 'p', "ㅔ", "ㅖ" }, }; Drawcmd dc; Str com; int i; for(i = 0; i < nelem(cases); i++){ init(); im.l = getlang(LangKO); sclear(&com); CT_CHECK(t, keystroke(cases[i].key, 0, &com)); checkstr(t, "plain Korean key", cases[i].plain, &im.pre); init(); im.l = getlang(LangKO); sclear(&com); CT_CHECK(t, keystroke(cases[i].key, Mshift, &com)); checkstr(t, "shifted Korean key", cases[i].shifted, &im.pre); init(); im.l = getlang(LangKO); sclear(&com); CT_CHECK(t, keystroke(cases[i].key - 'a' + 'A', 0, &com)); checkstr(t, "Caps Lock Korean key", cases[i].plain, &im.pre); init(); im.l = getlang(LangKO); sclear(&com); CT_CHECK(t, keystroke(cases[i].key, Mshift, &com)); checkstr(t, "Shift+Caps Korean key", cases[i].shifted, &im.pre); } init(); im.l = getlang(LangKO); sclear(&com); if(typekeys(t, "rkr", &com)){ checkstr(t, "Korean engine preedit", "각", &im.pre); draindraw(nil); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "Korean final backspace", "가", &im.pre); if(CT_CHECK(t, draindraw(&dc) == 1)) checkstr(t, "Korean backspace redraw", "가", &dc.pre); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "Korean vowel backspace", "ㄱ", &im.pre); CT_CHECK(t, keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, im.pre.n); CT_CHECK(t, !keystroke(Kback, 0, &com)); } } void engine_direct_language_modes(struct ct *t) { Str com; init(); sclear(&com); CT_EQ_INT(t, LangEN, im.l->lang); CT_EQ_PTR(t, nil, im.l->mapname); CT_EQ_PTR(t, nil, im.l->map); CT_EQ_PTR(t, nil, im.l->trans); CT_CHECK(t, !keystroke('a', 0, &com)); CT_CHECK(t, !keystroke('Z', Mshift, &com)); checkstr(t, "English direct commit", "", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, search.lang); CT_EQ_INT(t, LangEN, im.l->lang); CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, !search.lang); CT_CHECK(t, keystroke('s', Mctrl, &com)); CT_EQ_INT(t, LangKO, im.l->lang); CT_EQ_PTR(t, nil, im.l->mapname); CT_EQ_PTR(t, nil, im.l->map); CT_EQ_PTR(t, nil, im.l->dictname); CT_CHECK(t, strcmp(getlang(LangHANJA)->dictname, "hanja") == 0); CT_CHECK(t, keystroke('r', 0, &com)); checkstr(t, "Korean without map", "ㄱ", &im.pre); CT_CHECK(t, keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, im.pre.n); } void engine_language_switch_state(struct ct *t) { static const struct { char *name; Rune key; int mod; int lang; char *pre; char *raw; char *commit; int stale; } steps[] = { { "select Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 }, { "Vietnamese a", 'a', 0, LangVI, "a", "a", "", 0 }, { "Vietnamese tone", 's', 0, LangVI, "as", "as", "", 0 }, { "switch to Korean", 's', Mctrl, LangKO, "", "", "á", 1 }, { "Korean initial", 'r', 0, LangKO, "ㄱ", "", "", 0 }, { "Korean syllable", 'k', 0, LangKO, "가", "", "", 0 }, { "switch to Vietnamese", 'v', Mctrl, LangVI, "", "", "가", 0 }, { "Vietnamese a again", 'a', 0, LangVI, "a", "a", "", 0 }, { "Vietnamese tone again", 's', 0, LangVI, "as", "as", "", 0 }, { "commit Vietnamese", Kret, 0, LangVI, "", "", "á", 0 }, }; char where[80]; Lang *vi; Trie *saved; Str com; int eaten, i; vi = getlang(LangVI); if(!CT_CHECK(t, vi != nil)) return; saved = vi->map; vi->map = testvi.map; init(); for(i = 0; i < nelem(steps); i++){ if(steps[i].stale){ im.kouho[0] = mkstr("stale"); im.nkouho = 1; im.sel = 0; } sclear(&com); eaten = keystroke(steps[i].key, steps[i].mod, &com); if(eaten != 1){ CT_ERRORF(t, "%s: want eaten 1, got %d", steps[i].name, eaten); break; } if(im.l == nil || im.l->lang != steps[i].lang){ CT_ERRORF(t, "%s: want language %d, got %d", steps[i].name, steps[i].lang, im.l == nil ? -1 : im.l->lang); break; } snprint(where, sizeof where, "%s preedit", steps[i].name); if(!checkstr(t, where, steps[i].pre, &im.pre)) break; snprint(where, sizeof where, "%s raw", steps[i].name); if(!checkstr(t, where, steps[i].raw, &im.raw)) break; snprint(where, sizeof where, "%s commit", steps[i].name); if(!checkstr(t, where, steps[i].commit, &com)) break; if(im.nkouho != 0 || im.sel != -1){ CT_ERRORF(t, "%s: want candidates 0 and selection -1, got %d and %d", steps[i].name, im.nkouho, im.sel); break; } } init(); im.l = getlang(LangKO); sclear(&com); if(typekeys(t, "rk", &com)){ CT_CHECK(t, !keystroke('p', Mctrl, &com)); checkstr(t, "Korean Ctrl passthrough", "가", &com); CT_EQ_INT(t, 0, im.pre.n); } init(); im.l = &testvi; sclear(&com); if(typekeys(t, "as", &com)){ CT_CHECK(t, !keystroke('p', Mctrl, &com)); checkstr(t, "Vietnamese Ctrl passthrough", "á", &com); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); } vi->map = saved; } typedef struct Searchfix Searchfix; struct Searchfix { Im im; Search search; Lang *dictlang; Trie *dict; int activecap; Drawcmd lastdraw; int candidatechosen; }; static int draindraw(Drawcmd *last) { Drawcmd dc = {0}; int n; n = 0; while(channbrecv(drawc, &dc) > 0){ n++; if(last != nil) *last = dc; } return n; } static void setcandidates(int n) { char name[8]; int i; clearkouho(); for(i = 0; i < n; i++){ snprint(name, sizeof name, "c%d", i+1); im.kouho[i] = mkstr(name); } im.nkouho = n; selectfirst(); } static void candidatebegin(int n) { init(); draindraw(nil); setcandidates(n); } static int checkcandidatepage(struct ct *t, char *where, int first, int n, int sel) { Drawcmd dc; char want[8]; redraw(); if(!CT_CHECK(t, draindraw(&dc) > 0)) return 0; if(!CT_EQ_INT(t, n, dc.nkouho) || !CT_EQ_INT(t, sel, dc.sel) || !CT_EQ_INT(t, first, dc.first) || !CT_EQ_INT(t, im.nkouho, dc.total)) return 0; if(n == 0) return 1; snprint(want, sizeof want, "c%d", first+1); if(!checkstr(t, where, want, &dc.kouho[0])) return 0; snprint(want, sizeof want, "c%d", first+n); return checkstr(t, where, want, &dc.kouho[n-1]); } void engine_candidate_page_metadata(struct ct *t) { static const struct { int total; int sel; int first; int shown; int local; } cases[] = { { 1, 0, 0, 1, 0 }, { 9, 8, 0, 9, 8 }, { 10, 8, 0, 9, 8 }, { 10, 9, 9, 1, 0 }, { 18, 8, 0, 9, 8 }, { 18, 17, 9, 9, 8 }, { 19, 0, 0, 9, 0 }, { 32, 8, 0, 9, 8 }, { 32, 17, 9, 9, 8 }, { 32, 26, 18, 9, 8 }, { 32, 31, 27, 5, 4 }, }; int i; for(i = 0; i < nelem(cases); i++){ candidatebegin(cases[i].total); im.sel = cases[i].sel; checkcandidatepage(t, "candidate page metadata", cases[i].first, cases[i].shown, cases[i].local); } } void engine_candidate_page_snapshots(struct ct *t) { Drawcmd dc; /* Clearing a visible popup must publish an empty snapshot. */ candidatebegin(1); redraw(); draindraw(nil); setcandidates(0); redraw(); if(CT_CHECK(t, draindraw(&dc) > 0)){ CT_EQ_INT(t, 0, dc.nkouho); CT_EQ_INT(t, -1, dc.sel); CT_EQ_INT(t, 0, dc.first); CT_EQ_INT(t, 0, dc.total); } } void engine_candidate_page_movement(struct ct *t) { static const struct { int n; int sel; Rune key; int want; } cases[] = { { 1, 0, Kup, 0 }, { 9, 8, Kdown, 8 }, { 10, 8, Kdown, 9 }, { 10, 9, Kup, 8 }, { 18, 8, Kpgdown, 17 }, { 19, 17, Kdown, 18 }, { 19, 18, Kup, 17 }, { 32, 26, Kdown, 27 }, { 32, 27, Kup, 26 }, { 32, 0, Kpgup, 0 }, { 32, 9, Kpgup, 0 }, { 32, 22, Kpgdown, 31 }, { 32, 31, Kpgup, 22 }, { 32, 31, Kpgdown, 31 }, }; int first, i, n; Str com; for(i = 0; i < nelem(cases); i++){ candidatebegin(cases[i].n); im.sel = cases[i].sel; sclear(&com); CT_CHECK(t, keystroke(cases[i].key, 0, &com)); CT_EQ_INT(t, cases[i].want, im.sel); first = cases[i].want / Maxdisp * Maxdisp; n = cases[i].n - first; if(n > Maxdisp) n = Maxdisp; checkcandidatepage(t, "moved candidate page", first, n, cases[i].want-first); } } void engine_candidate_completion(struct ct *t) { static Rune keys[] = { Kret, Ktab }; Str com; int i; for(i = 0; i < nelem(keys); i++){ init(); im.l = getlang(LangJP); im.pre = mkstr("reading"); setcandidates(1); sclear(&com); CT_CHECK(t, keystroke(keys[i], 0, &com)); checkstr(t, "unselected candidate commits reading", "reading", &com); CT_EQ_INT(t, 0, im.nkouho); } candidatebegin(2); im.pre = mkstr("reading"); sclear(&com); CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "explicit candidate", "c2", &com); candidatebegin(2); im.pre = mkstr("reading"); sclear(&com); CT_CHECK(t, keystroke('0', 0, &com)); checkstr(t, "ordinary zero reading", "reading", &com); candidatebegin(0); im.pre = mkstr("reading"); sclear(&com); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "Enter without candidates", "reading", &com); candidatebegin(32); im.sel = 27; sclear(&com); CT_CHECK(t, !keystroke('9', 0, &com)); checkstr(t, "unavailable short-page digit", "", &com); CT_EQ_INT(t, 32, im.nkouho); CT_EQ_INT(t, 27, im.sel); } static void setdict(Trie *dict, char *key, char *val) { trieput(dict, key, strlen(key), val, strlen(val)); } static void searchsave(Searchfix *f, int lang) { f->im = im; f->search = search; f->dictlang = getlang(lang); f->dict = f->dictlang->dict; f->activecap = activecap; f->lastdraw = lastdraw; f->candidatechosen = candidatechosen; draindraw(nil); } static void emojibegin(Searchfix *f, int showpre) { searchsave(f, LangEMOJI); f->dictlang->dict = trienew(); setdict(f->dictlang->dict, "a", "A B"); setdict(f->dictlang->dict, "ㅁ", "B C"); setdict(f->dictlang->dict, "alpha", "α"); setdict(f->dictlang->dict, "smil", "😀 😄"); setdict(f->dictlang->dict, "smile", "😀 😄"); setdict(f->dictlang->dict, "웃음", "😀 😄 😂"); setdict(f->dictlang->dict, "えがお", "😀 😄 😊"); setdict(f->dictlang->dict, "エガオ", "😀 😄 😊"); setdict(f->dictlang->dict, "heart", "❤️"); setdict(f->dictlang->dict, "coffee", "☕️"); setdict(f->dictlang->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽"); setdict(f->dictlang->dict, "^0", "⁰"); setdict(f->dictlang->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍"); setdict(f->dictlang->dict, "<", "← ≤ ♥ ≠"); setdict(f->dictlang->dict, "many", "c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12"); init(); im.l = getlang(LangKO); activecap = showpre ? 0 : Cclientpreedit; } static void searchend(Searchfix *f) { Trie *dict; dict = f->dictlang->dict; f->dictlang->dict = f->dict; trieclose(dict); draindraw(nil); im = f->im; search = f->search; activecap = f->activecap; lastdraw = f->lastdraw; candidatechosen = f->candidatechosen; } void engine_popup_preedit_capability(struct ct *t) { Drawcmd dc; Keyres kres; Lang *jp; Trie *saved; char client, inactive; jp = getlang(LangJP); saved = jp->dict; jp->dict = trienew(); setdict(jp->dict, "か", "家"); init(); im.l = jp; draindraw(nil); ownerrequestcap(&client, Cclientpreedit, Keypress, 'k', 0); ownerrequestcap(&client, Cclientpreedit, Keypress, 'a', 0); CT_EQ_INT(t, 1, im.nkouho); redraw(); if(CT_CHECK(t, draindraw(&dc) > 0)){ CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 0, dc.sel); checkstr(t, "client-preedit candidate", "家", &dc.kouho[0]); } kres = ownerrequestcap(&client, 0, Keycap, 0, 0); CT_CHECK(t, kres.eaten); checkstr(t, "capability response preedit", "か", &kres.preedit); if(CT_CHECK(t, draindraw(&dc) > 0)){ checkstr(t, "popup preedit", "か", &dc.pre); CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 0, dc.sel); checkstr(t, "popup candidate", "家", &dc.kouho[0]); } CT_EQ_INT(t, 1, im.nkouho); CT_EQ_INT(t, 0, im.sel); checkstr(t, "retained popup candidate", "家", &im.kouho[0]); kres = ownerrequestcap(&client, 0, Keycap, 0, 0); CT_CHECK(t, kres.eaten); CT_EQ_INT(t, 0, draindraw(nil)); kres = ownerrequestcap(&inactive, Cclientpreedit, Keycap, 0, 0); CT_CHECK(t, !kres.eaten); CT_EQ_INT(t, 0, kres.preedit.n); CT_EQ_PTR(t, &client, activeowner); CT_EQ_INT(t, 0, activecap & Cclientpreedit); CT_EQ_INT(t, 0, draindraw(nil)); kres = ownerrequestcap(&client, Cclientpreedit, Keycap, 0, 0); CT_CHECK(t, kres.eaten); checkstr(t, "restored client preedit", "か", &kres.preedit); if(CT_CHECK(t, draindraw(&dc) > 0)){ CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 0, dc.sel); checkstr(t, "restored client candidate", "家", &dc.kouho[0]); } CT_EQ_INT(t, 1, im.nkouho); CT_EQ_INT(t, 0, im.sel); checkstr(t, "retained client candidate", "家", &im.kouho[0]); kres = ownerrequestcap(&client, Cclientpreedit, Keycap, 0, 0); CT_CHECK(t, kres.eaten); CT_EQ_INT(t, 0, draindraw(nil)); ownerrequest(&client, Keyrelease, 0, 0); draindraw(nil); trieclose(jp->dict); jp->dict = saved; } static void hanjabegin(Searchfix *f, int lang) { searchsave(f, LangHANJA); f->dictlang->dict = trienew(); setdict(f->dictlang->dict, "한", "漢 韓"); setdict(f->dictlang->dict, "가", "家"); init(); im.l = getlang(lang); } static int typekeys(struct ct *t, char *keys, Str *com) { for(; *keys != '\0'; keys++) if(!CT_CHECK(t, keystroke((uchar)*keys, 0, com))) return 0; return 1; } void engine_japanese_readings(struct ct *t) { static const struct { char *keys; char *want; } cases[] = { { "nya", "にゃ" }, { "n'ya", "んや" }, { "nnya", "んにゃ" }, { "kin'youbi", "きんようび" }, }; static const struct { char *raw; char *want; } replay[] = { { "kitte", "きって" }, { "kanj", "かんj" }, { "n'ya", "んや" }, }; Str com, raw, shown; int i; for(i = 0; i < nelem(cases); i++){ init(); im.l = getlang(LangJP); sclear(&com); if(!typekeys(t, cases[i].keys, &com)) continue; checkstr(t, cases[i].keys, "", &com); impre(&im, &shown); checkstr(t, cases[i].keys, cases[i].want, &shown); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "committed reading", cases[i].want, &com); } for(i = 0; i < nelem(replay); i++){ raw = mkstr(replay[i].raw); transstr(getlang(LangJP), &raw, &shown); checkstr(t, replay[i].raw, replay[i].want, &shown); } } void engine_japanese_candidates(struct ct *t) { Trie *saved; Lang *jp; Str com, shown; jp = getlang(LangJP); saved = jp->dict; jp->dict = trienew(); setdict(jp->dict, "かんじ", "漢字 幹事"); init(); im.l = jp; sclear(&com); if(!typekeys(t, "kanji", &com)) goto cleanup; impre(&im, &shown); checkstr(t, "complete Japanese reading", "かんじ", &shown); if(!CT_EQ_INT(t, 2, im.nkouho)) goto cleanup; CT_EQ_INT(t, 0, im.sel); checkstr(t, "candidate one", "漢字", &im.kouho[0]); checkstr(t, "candidate two", "幹事", &im.kouho[1]); CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "selected Kanji", "幹事", &com); CT_EQ_INT(t, 0, im.nkouho); /* Backspace looks a complete shorter reading up again. */ setdict(jp->dict, "かん", "缶"); if(!typekeys(t, "kanji", &com)) goto cleanup; CT_CHECK(t, keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, im.nkouho); CT_CHECK(t, keystroke(Kback, 0, &com)); if(CT_EQ_INT(t, 1, im.nkouho)) checkstr(t, "shorter reading candidate", "缶", &im.kouho[0]); cleanup: trieclose(jp->dict); jp->dict = saved; } void engine_japanese_backspace_and_boundaries(struct ct *t) { Str com, shown; init(); im.l = getlang(LangJP); sclear(&com); if(typekeys(t, "kanji", &com)){ CT_CHECK(t, keystroke(Kback, 0, &com)); impre(&im, &shown); checkstr(t, "backspace pending i", "かんj", &shown); CT_CHECK(t, keystroke(Kback, 0, &com)); impre(&im, &shown); checkstr(t, "backspace pending j", "かん", &shown); CT_CHECK(t, keystroke(Kback, 0, &com)); impre(&im, &shown); checkstr(t, "backspace accumulated kana", "か", &shown); } init(); im.l = getlang(LangJP); sclear(&com); if(typekeys(t, "kanji", &com)){ CT_CHECK(t, keystroke('k', Mctrl, &com)); checkstr(t, "Hiragana mode boundary", "かんじ", &com); CT_EQ_INT(t, LangJPK, im.l->lang); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); sclear(&com); if(typekeys(t, "ka", &com)){ CT_CHECK(t, keystroke('n', Mctrl, &com)); checkstr(t, "Katakana mode boundary", "カ", &com); CT_EQ_INT(t, LangJP, im.l->lang); } } init(); im.l = getlang(LangJP); sclear(&com); if(typekeys(t, "kanji", &com)){ CT_CHECK(t, keystroke(' ', 0, &com)); checkstr(t, "real input boundary", "かんじ ", &com); } } void engine_katakana_sequences(struct ct *t) { static const struct { char *keys; char *want; } cases[] = { { "kitte", "キッテ" }, { "katta", "カッタ" }, { "gakkou", "ガッコウ" }, { "matcha", "マッチャ" }, { "xu", "ゥ" }, { "vu", "ヴ" }, }; Str com, shown; int i; for(i = 0; i < nelem(cases); i++){ init(); im.l = getlang(LangJPK); sclear(&com); if(!typekeys(t, cases[i].keys, &com)) continue; checkstr(t, cases[i].keys, "", &com); impre(&im, &shown); checkstr(t, cases[i].keys, cases[i].want, &shown); CT_EQ_INT(t, 0, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "committed Katakana", cases[i].want, &com); } CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname); } void engine_emoji_single_candidate(struct ct *t) { Drawcmd dc = {0}; Searchfix f; Str com; emojibegin(&f, 0); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); if(typekeys(t, "alpha", &com)){ checkstr(t, "no automatic commit", "", &com); checkstr(t, "raw English query", "alpha", &search.text); CT_CHECK(t, search.lang); CT_EQ_INT(t, 1, im.nkouho); checkstr(t, "single candidate", "α", &im.kouho[0]); CT_CHECK(t, draindraw(&dc) > 0); CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 1, dc.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "explicit candidate commit", "α", &com); CT_CHECK(t, !search.lang); CT_EQ_INT(t, LangKO, im.l->lang); redraw(); CT_CHECK(t, draindraw(&dc) > 0); CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 0, dc.nkouho); } searchend(&f); } void engine_emoji_queries(struct ct *t) { Drawcmd dc = {0}; Searchfix f; Str com; emojibegin(&f, 1); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('a', 0, &com)); checkstr(t, "raw query wins", "a", &search.text); CT_EQ_INT(t, 3, im.nkouho); checkstr(t, "raw candidate", "A", &im.kouho[0]); checkstr(t, "deduplicated candidate", "B", &im.kouho[1]); checkstr(t, "local candidate", "C", &im.kouho[2]); CT_CHECK(t, draindraw(&dc) > 0); checkstr(t, "prefix popup query", "a", &dc.pre); CT_EQ_INT(t, 3, dc.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ checkstr(t, "English alias", "smile", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "backspaced query", "smil", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke('e', 0, &com)); CT_CHECK(t, keystroke('2', 0, &com)); checkstr(t, "selected English alias", "😄", &com); CT_EQ_INT(t, LangKO, im.l->lang); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "SMILE", &com)){ checkstr(t, "folded English alias", "smile", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "dntdma", &com)){ checkstr(t, "localized Korean query", "웃음", &search.text); CT_EQ_INT(t, 3, im.nkouho); CT_CHECK(t, draindraw(&dc) > 0); checkstr(t, "popup query", "웃음", &dc.pre); CT_EQ_INT(t, 3, dc.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); checkstr(t, "cancel commit", "", &com); CT_CHECK(t, !search.lang); CT_EQ_INT(t, LangKO, im.l->lang); } searchend(&f); } void engine_emoji_japanese_and_multirune(struct ct *t) { Searchfix f; Str com; emojibegin(&f, 0); init(); im.l = getlang(LangJP); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "egao", &com)){ checkstr(t, "localized Hiragana query", "えがお", &search.text); CT_EQ_INT(t, 3, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); } init(); im.l = getlang(LangJPK); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "egao", &com)){ checkstr(t, "localized Katakana query", "エガオ", &search.text); CT_EQ_INT(t, 3, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); } init(); im.l = getlang(LangEN); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "heart", &com)){ CT_EQ_INT(t, 1, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "variation-selector emoji", "❤️", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "coffee", &com)){ CT_EQ_INT(t, 1, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "multi-codepoint emoji", "☕️", &com); } searchend(&f); } void engine_emoji_digit_aliases(struct ct *t) { static const struct { char *name; Rune prefix; Rune digit; char *want; } cases[] = { { "^1", '^', '1', "¹" }, { "_2", '_', '2', "₂" }, { "<3", '<', '3', "♥" }, }; Searchfix f; Str com; int i; emojibegin(&f, 0); for(i = 0; i < nelem(cases); i++){ sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke(cases[i].prefix, Mshift, &com)); CT_CHECK(t, im.nkouho >= cases[i].digit - '0'); CT_CHECK(t, keystroke(cases[i].digit, 0, &com)); checkstr(t, cases[i].name, cases[i].want, &com); CT_CHECK(t, !search.lang); CT_EQ_INT(t, LangKO, im.l->lang); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('^', Mshift, &com)); CT_CHECK(t, keystroke('0', 0, &com)); checkstr(t, "zero query", "^0", &search.text); checkstr(t, "zero does not commit", "", &com); CT_EQ_INT(t, 1, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "zero alias", "⁰", &com); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ CT_CHECK(t, keystroke('9', 0, &com)); checkstr(t, "missing number query", "smile9", &search.text); checkstr(t, "missing number commit", "", &com); CT_CHECK(t, search.lang); CT_EQ_INT(t, 0, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); } searchend(&f); } void engine_emoji_navigation(struct ct *t) { Drawcmd dc = {0}; Searchfix f; Str com; emojibegin(&f, 0); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "many", &com)){ CT_EQ_INT(t, 12, im.nkouho); CT_EQ_INT(t, 0, im.sel); CT_CHECK(t, keystroke(Kpgdown, 0, &com)); CT_EQ_INT(t, Maxdisp, im.sel); CT_CHECK(t, draindraw(&dc) > 0); checkstr(t, "second-page first row", "c10", &dc.kouho[0]); checkstr(t, "second-page last row", "c12", &dc.kouho[2]); CT_EQ_INT(t, 3, dc.nkouho); CT_EQ_INT(t, 0, dc.sel); CT_CHECK(t, keystroke('1', 0, &com)); checkstr(t, "second-page row one", "c10", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ CT_EQ_INT(t, 0, im.sel); CT_CHECK(t, keystroke(Ktab, Mshift, &com)); CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Ktab, 0, &com)); CT_EQ_INT(t, 0, im.sel); CT_CHECK(t, keystroke(Kesc, 0, &com)); } searchend(&f); } void engine_search_candidate_keys(struct ct *t) { Searchfix f; Str com; emojibegin(&f, 0); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ CT_EQ_INT(t, 0, im.sel); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "search default Enter", "😀", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "missing", &com)){ CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, -1, im.sel); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "search query Enter", "missing", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('^', Mshift, &com)); CT_CHECK(t, keystroke('0', 0, &com)); checkstr(t, "search zero query", "^0", &search.text); checkstr(t, "search zero commit", "", &com); CT_CHECK(t, search.lang); CT_CHECK(t, keystroke(Kesc, 0, &com)); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "many", &com)){ im.sel = Maxdisp; CT_CHECK(t, keystroke('1', Mshift, &com)); checkstr(t, "modified search digit commit", "", &com); checkstr(t, "modified search digit query", "many1", &search.text); CT_CHECK(t, search.lang); CT_CHECK(t, keystroke(Kesc, 0, &com)); } searchend(&f); } void engine_emoji_preedit_languages(struct ct *t) { Searchfix f; Str com, shown; emojibegin(&f, 0); init(); im.l = getlang(LangJP); sclear(&com); if(typekeys(t, "ka", &com)){ CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); impre(&im, &shown); checkstr(t, "modifier preserves Japanese", "か", &shown); CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); checkstr(t, "committed Japanese preedit", "か", &com); CT_CHECK(t, search.lang); CT_EQ_INT(t, LangJP, im.l->lang); CT_CHECK(t, !keystroke('E', Mctrl|Malt, &com)); CT_EQ_INT(t, 0, search.lang); CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com)); CT_EQ_INT(t, 0, search.lang); CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_EQ_INT(t, LangJP, im.l->lang); CT_CHECK(t, keystroke('a', 0, &com)); CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); CT_CHECK(t, search.lang); checkstr(t, "modifier preserves search", "a", &search.text); CT_CHECK(t, !keystroke('P', Mctrl|Mshift, &com)); CT_CHECK(t, !search.lang); sclear(&com); if(typekeys(t, "na", &com)){ impre(&im, &shown); checkstr(t, "Japanese resumed", "な", &shown); } } init(); im.l = getlang(LangKO); sclear(&com); if(typekeys(t, "rk", &com)){ CT_CHECK(t, keystroke('e', Mctrl, &com)); checkstr(t, "committed Korean preedit", "가", &com); if(typekeys(t, "smile", &com)){ CT_CHECK(t, keystroke('1', 0, &com)); checkstr(t, "Korean selection", "가😀", &com); sclear(&com); if(typekeys(t, "rk", &com)) checkstr(t, "Korean resumed", "가", &im.pre); } } init(); im.l = &testvi; sclear(&com); if(typekeys(t, "as", &com)){ CT_CHECK(t, keystroke('e', Mctrl, &com)); checkstr(t, "committed Telex preedit", "á", &com); CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, !search.lang); CT_EQ_PTR(t, &testvi, im.l); sclear(&com); if(typekeys(t, "as", &com)){ impre(&im, &shown); checkstr(t, "Telex resumed", "á", &shown); checkstr(t, "Telex raw resumed", "as", &im.raw); } } searchend(&f); } void engine_emoji_start_and_unknown(struct ct *t) { Searchfix f; Str com; char want[Maxrunes+1]; int i; emojibegin(&f, 0); sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "xyz", &com)){ CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com)); checkstr(t, "special commits shown query", "xyz", &com); CT_CHECK(t, !search.lang); CT_EQ_INT(t, LangKO, im.l->lang); } /* A language switch commits the query like any other pending text. */ sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "xy", &com)){ CT_CHECK(t, keystroke('t', Mctrl, &com)); checkstr(t, "language switch commits query", "xy", &com); CT_CHECK(t, !search.lang); CT_EQ_INT(t, LangEN, im.l->lang); im.l = getlang(LangKO); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); for(i = 0; i < Maxrunes; i++) CT_CHECK(t, keystroke('q', 0, &com)); CT_EQ_INT(t, Maxrunes, search.raw.n); CT_CHECK(t, search.lang); CT_CHECK(t, !keystroke('z', 0, &com)); memset(want, 'q', Maxrunes); want[Maxrunes] = '\0'; checkstr(t, "bounded query commit", want, &com); CT_CHECK(t, !search.lang); searchend(&f); } void engine_hanja_search(struct ct *t) { Searchfix f; Str com; hanjabegin(&f, LangKO); sclear(&com); if(!CT_CHECK(t, keystroke('h', Mctrl, &com))) goto cleanup; CT_EQ_INT(t, LangHANJA, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); if(!typekeys(t, "gks", &com)) goto cleanup; checkstr(t, "Hanja reading", "한", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "navigated Hanja", "韓", &com); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); cleanup: searchend(&f); } void engine_hanja_unknown_and_cancel(struct ct *t) { static const struct { char *keys; char *text; } empty[] = { { "rmf", "글" }, { "r", "ㄱ" }, { "gksrmf", "한글" }, }; Searchfix f; Str com; int i; hanjabegin(&f, LangKO); for(i = 0; i < nelem(empty); i++){ sclear(&com); if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) || !typekeys(t, empty[i].keys, &com)) goto cleanup; checkstr(t, "Hanja reading without candidates", empty[i].text, &search.text); CT_EQ_INT(t, 0, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "Hanja commit without candidates", empty[i].text, &com); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); } sclear(&com); if(!CT_CHECK(t, keystroke('h', Mctrl, &com))) goto cleanup; if(!typekeys(t, "gks", &com)) goto cleanup; CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); checkstr(t, "cancelled Hanja", "", &com); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); sclear(&com); CT_CHECK(t, keystroke('h', Mctrl, &com)); CT_EQ_INT(t, LangHANJA, search.lang); CT_CHECK(t, keystroke('h', Mctrl, &com)); CT_EQ_INT(t, 0, search.lang); cleanup: searchend(&f); } void engine_hanja_korean_keys(struct ct *t) { Searchfix f; Str com; hanjabegin(&f, LangKO); sclear(&com); CT_CHECK(t, keystroke('h', Mctrl, &com)); CT_CHECK(t, keystroke('r', Mshift, &com)); CT_CHECK(t, keystroke('k', 0, &com)); checkstr(t, "shifted Hanja reading", "까", &search.text); CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, keystroke('h', Mctrl, &com)); CT_CHECK(t, keystroke('R', 0, &com)); CT_CHECK(t, keystroke('k', 0, &com)); checkstr(t, "Caps Lock Hanja reading", "가", &search.text); CT_EQ_INT(t, 1, im.nkouho); CT_CHECK(t, keystroke(Kesc, 0, &com)); searchend(&f); } void engine_hanja_backspace(struct ct *t) { Searchfix f; Str com; hanjabegin(&f, LangKO); sclear(&com); CT_CHECK(t, keystroke('h', Mctrl, &com)); if(!typekeys(t, "gks", &com)) goto cleanup; checkstr(t, "Hanja before backspace", "한", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "Hanja final backspace", "하", &search.text); CT_EQ_INT(t, 0, im.nkouho); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "Hanja vowel backspace", "ㅎ", &search.text); CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "empty Hanja search", "", &search.text); CT_EQ_INT(t, LangHANJA, search.lang); CT_CHECK(t, !keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, search.lang); if(!typekeys(t, "gks", &com)) goto cleanup; CT_CHECK(t, keystroke('h', Mctrl, &com)); checkstr(t, "preedit committed before Hanja", "한", &com); CT_EQ_INT(t, LangHANJA, search.lang); CT_CHECK(t, !keystroke(Kback, 0, &com)); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangKO, im.l->lang); cleanup: searchend(&f); } void engine_hanja_input_languages(struct ct *t) { Searchfix f; Str com; hanjabegin(&f, LangEN); sclear(&com); if(!CT_CHECK(t, keystroke('h', Mctrl, &com))) goto cleanup; if(!typekeys(t, "gks", &com)) goto cleanup; checkstr(t, "English Hanja input", "gks", &search.text); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, LangEN, im.l->lang); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "English Hanja commit", "gks", &com); CT_EQ_INT(t, LangEN, im.l->lang); init(); im.l = getlang(LangJP); sclear(&com); if(!CT_CHECK(t, keystroke('h', Mctrl, &com))) goto cleanup; if(!typekeys(t, "ka", &com)) goto cleanup; checkstr(t, "Japanese Hanja input", "か", &search.text); CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, LangJP, im.l->lang); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "Japanese Hanja commit", "か", &com); CT_EQ_INT(t, LangJP, im.l->lang); init(); im.l = getlang(LangEN); sclear(&com); CT_CHECK(t, keystroke('h', Mctrl, &com)); CT_CHECK(t, keystroke(L'한', 0, &com)); checkstr(t, "direct Hanja input", "한", &search.text); CT_EQ_INT(t, 2, im.nkouho); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "direct Hanja selection", "漢", &com); CT_EQ_INT(t, LangEN, im.l->lang); init(); im.l = getlang(LangJP); sclear(&com); CT_CHECK(t, keystroke('h', Mctrl, &com)); if(!typekeys(t, "ka", &com)) goto cleanup; CT_CHECK(t, !keystroke('e', Mctrl|Malt, &com)); checkstr(t, "modified search boundary", "か", &com); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangJP, im.l->lang); sclear(&com); CT_CHECK(t, !keystroke('h', Mctrl|Msuper, &com)); CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, LangJP, im.l->lang); cleanup: searchend(&f); } static u32int stressrand(u32int *state) { *state = *state * 1664525U + 1013904223U; return *state; } static int stressstr(Str *s) { return s->n >= 0 && s->n <= Maxrunes; } void engine_randomized_stress(struct ct *t) { static Rune text[] = { 'a', 'e', 'g', 'i', 'j', 'k', 'm', 'n', 'o', 'r', 's', 't', 'u', 'w', 'y', '1', '2', '9', '\'', ' ', L'日', }; static Rune modes[] = { 't', 'n', 'k', 's', 'v' }; Searchfix f; Lang *vi; Trie *vimap; Keyres res; Caret pos; u32int rnd, mod; char contexts[3]; void *owner, *owners[3]; int i, j, op; vi = getlang(LangVI); if(!CT_CHECK(t, vi != nil)) return; vimap = vi->map; vi->map = testvi.map; emojibegin(&f, 0); owners[0] = &contexts[0]; owners[1] = &contexts[1]; owners[2] = &contexts[2]; rnd = 0x5eed1234U; for(i = 0; i < 16000; i++){ rnd = stressrand(&rnd); owner = owners[(rnd >> 8) % nelem(owners)]; op = rnd % 28; mod = (rnd >> 16) & Mmask; memset(&pos, 0, sizeof pos); pos.valid = (rnd >> 25) & 1; pos.x = (rnd >> 3) & 0x3ff; pos.y = (rnd >> 13) & 0x3ff; pos.h = 1 + ((rnd >> 18) & 0x1f); if(op < 13) res = ownerrequestat(owner, Keypress, text[(rnd >> 5) % nelem(text)], mod, &pos); else if(op == 13) res = ownerrequestat(owner, Keypress, Kback, 0, &pos); else if(op == 14) res = ownerrequestat(owner, Keypress, Kret, 0, &pos); else if(op == 15) res = ownerrequestat(owner, Keypress, Kesc, 0, &pos); else if(op == 16) res = ownerrequestat(owner, Keypress, Kdown, 0, &pos); else if(op == 17) res = ownerrequestat(owner, Keypress, Kup, 0, &pos); else if(op == 18) res = ownerrequestat(owner, Keypress, Ktab, (rnd & 0x1000) ? Mshift : 0, &pos); else if(op == 19) res = ownerrequestat(owner, Keypress, modes[(rnd >> 12) % nelem(modes)], Mctrl, &pos); else if(op == 20) res = ownerrequestat(owner, Keypress, 'e', Mctrl, &pos); else if(op == 21) res = ownerrequest(owner, Keyreset, 0, 0); else if(op == 22) res = ownerrequest(owner, Keyrelease, 0, 0); else if(op == 23) res = ownerrequestat(owner, Keycaret, 0, 0, &pos); else if(op == 24) res = ownerrequest(owner, Keypress, Kmodfirst + ((rnd >> 20) & 7), 0); else if(op == 25) res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos); else if(op == 26) res = ownerrequestat(owner, Keypress, 0xf008, 0, &pos); else res = ownerrequestat(owner, Keypress, '1' + ((rnd >> 11) % 9), 0, &pos); if(!stressstr(&res.commit) || !stressstr(&res.preedit) || !stressstr(&im.pre) || !stressstr(&im.raw) || !stressstr(&search.raw) || !stressstr(&search.text) || im.l == nil || im.nkouho < 0 || im.nkouho > Maxkouho || im.sel < -1 || im.sel >= Maxkouho || (im.nkouho > 0 && im.sel >= im.nkouho)){ CT_ERRORF(t, "invalid engine state after stress operation %d", i); break; } for(j = 0; j < im.nkouho; j++) if(!stressstr(&im.kouho[j])){ CT_ERRORF(t, "invalid candidate after stress operation %d", i); i = 16000; break; } if(activeowner != nil && activeowner != owners[0] && activeowner != owners[1] && activeowner != owners[2]){ CT_ERRORF(t, "unknown active owner after stress operation %d", i); break; } } searchend(&f); vi->map = vimap; } void engine_full_boundary_passthrough(struct ct *t) { Str com; int i; init(); im.l = getlang(LangJP); for(i = 0; i < Maxrunes; i++) im.pre.r[i] = L'あ'; im.pre.n = Maxrunes; sclear(&com); CT_CHECK(t, !keystroke(' ', 0, &com)); CT_EQ_INT(t, Maxrunes, com.n); for(i = 0; i < Maxrunes; i++) CT_EQ_UINT(t, L'あ', com.r[i]); CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.raw.n); init(); im.l = getlang(LangJP); for(i = 0; i < Maxrunes-1; i++) im.pre.r[i] = L'あ'; im.pre.n = Maxrunes-1; sclear(&com); CT_CHECK(t, keystroke(' ', 0, &com)); CT_EQ_INT(t, Maxrunes, com.n); CT_EQ_UINT(t, ' ', com.r[Maxrunes-1]); }