diff --git a/src/test/escape_sequence_files/NOTES b/fixtures/escape_sequence_files/NOTES similarity index 100% rename from src/test/escape_sequence_files/NOTES rename to fixtures/escape_sequence_files/NOTES diff --git a/src/test/escape_sequence_files/t0001-all_printable.in b/fixtures/escape_sequence_files/t0001-all_printable.in similarity index 100% rename from src/test/escape_sequence_files/t0001-all_printable.in rename to fixtures/escape_sequence_files/t0001-all_printable.in diff --git a/src/test/escape_sequence_files/t0001-all_printable.text b/fixtures/escape_sequence_files/t0001-all_printable.text similarity index 100% rename from src/test/escape_sequence_files/t0001-all_printable.text rename to fixtures/escape_sequence_files/t0001-all_printable.text diff --git a/src/test/escape_sequence_files/t0002-history.in b/fixtures/escape_sequence_files/t0002-history.in similarity index 100% rename from src/test/escape_sequence_files/t0002-history.in rename to fixtures/escape_sequence_files/t0002-history.in diff --git a/src/test/escape_sequence_files/t0002-history.text b/fixtures/escape_sequence_files/t0002-history.text similarity index 100% rename from src/test/escape_sequence_files/t0002-history.text rename to fixtures/escape_sequence_files/t0002-history.text diff --git a/src/test/escape_sequence_files/t0002j-simple_string.in b/fixtures/escape_sequence_files/t0002j-simple_string.in similarity index 100% rename from src/test/escape_sequence_files/t0002j-simple_string.in rename to fixtures/escape_sequence_files/t0002j-simple_string.in diff --git a/src/test/escape_sequence_files/t0002j-simple_string.text b/fixtures/escape_sequence_files/t0002j-simple_string.text similarity index 100% rename from src/test/escape_sequence_files/t0002j-simple_string.text rename to fixtures/escape_sequence_files/t0002j-simple_string.text diff --git a/src/test/escape_sequence_files/t0003-line_wrap.in b/fixtures/escape_sequence_files/t0003-line_wrap.in similarity index 100% rename from src/test/escape_sequence_files/t0003-line_wrap.in rename to fixtures/escape_sequence_files/t0003-line_wrap.in diff --git a/src/test/escape_sequence_files/t0003-line_wrap.text b/fixtures/escape_sequence_files/t0003-line_wrap.text similarity index 100% rename from src/test/escape_sequence_files/t0003-line_wrap.text rename to fixtures/escape_sequence_files/t0003-line_wrap.text diff --git a/src/test/escape_sequence_files/t0003j-LF.in b/fixtures/escape_sequence_files/t0003j-LF.in similarity index 100% rename from src/test/escape_sequence_files/t0003j-LF.in rename to fixtures/escape_sequence_files/t0003j-LF.in diff --git a/src/test/escape_sequence_files/t0003j-LF.text b/fixtures/escape_sequence_files/t0003j-LF.text similarity index 100% rename from src/test/escape_sequence_files/t0003j-LF.text rename to fixtures/escape_sequence_files/t0003j-LF.text diff --git a/src/test/escape_sequence_files/t0004-LF.in b/fixtures/escape_sequence_files/t0004-LF.in similarity index 100% rename from src/test/escape_sequence_files/t0004-LF.in rename to fixtures/escape_sequence_files/t0004-LF.in diff --git a/src/test/escape_sequence_files/t0004-LF.text b/fixtures/escape_sequence_files/t0004-LF.text similarity index 100% rename from src/test/escape_sequence_files/t0004-LF.text rename to fixtures/escape_sequence_files/t0004-LF.text diff --git a/src/test/escape_sequence_files/t0004j-CR.in b/fixtures/escape_sequence_files/t0004j-CR.in similarity index 100% rename from src/test/escape_sequence_files/t0004j-CR.in rename to fixtures/escape_sequence_files/t0004j-CR.in diff --git a/src/test/escape_sequence_files/t0004j-CR.text b/fixtures/escape_sequence_files/t0004j-CR.text similarity index 100% rename from src/test/escape_sequence_files/t0004j-CR.text rename to fixtures/escape_sequence_files/t0004j-CR.text diff --git a/src/test/escape_sequence_files/t0005-CR.in b/fixtures/escape_sequence_files/t0005-CR.in similarity index 100% rename from src/test/escape_sequence_files/t0005-CR.in rename to fixtures/escape_sequence_files/t0005-CR.in diff --git a/src/test/escape_sequence_files/t0005-CR.text b/fixtures/escape_sequence_files/t0005-CR.text similarity index 100% rename from src/test/escape_sequence_files/t0005-CR.text rename to fixtures/escape_sequence_files/t0005-CR.text diff --git a/src/test/escape_sequence_files/t0006-IND.in b/fixtures/escape_sequence_files/t0006-IND.in similarity index 100% rename from src/test/escape_sequence_files/t0006-IND.in rename to fixtures/escape_sequence_files/t0006-IND.in diff --git a/src/test/escape_sequence_files/t0006-IND.text b/fixtures/escape_sequence_files/t0006-IND.text similarity index 100% rename from src/test/escape_sequence_files/t0006-IND.text rename to fixtures/escape_sequence_files/t0006-IND.text diff --git a/src/test/escape_sequence_files/t0007-space_at_end.in b/fixtures/escape_sequence_files/t0007-space_at_end.in similarity index 100% rename from src/test/escape_sequence_files/t0007-space_at_end.in rename to fixtures/escape_sequence_files/t0007-space_at_end.in diff --git a/src/test/escape_sequence_files/t0007-space_at_end.text b/fixtures/escape_sequence_files/t0007-space_at_end.text similarity index 100% rename from src/test/escape_sequence_files/t0007-space_at_end.text rename to fixtures/escape_sequence_files/t0007-space_at_end.text diff --git a/src/test/escape_sequence_files/t0008-BS.in b/fixtures/escape_sequence_files/t0008-BS.in similarity index 100% rename from src/test/escape_sequence_files/t0008-BS.in rename to fixtures/escape_sequence_files/t0008-BS.in diff --git a/src/test/escape_sequence_files/t0008-BS.text b/fixtures/escape_sequence_files/t0008-BS.text similarity index 100% rename from src/test/escape_sequence_files/t0008-BS.text rename to fixtures/escape_sequence_files/t0008-BS.text diff --git a/src/test/escape_sequence_files/t0009-NEL.in b/fixtures/escape_sequence_files/t0009-NEL.in similarity index 100% rename from src/test/escape_sequence_files/t0009-NEL.in rename to fixtures/escape_sequence_files/t0009-NEL.in diff --git a/src/test/escape_sequence_files/t0009-NEL.text b/fixtures/escape_sequence_files/t0009-NEL.text similarity index 100% rename from src/test/escape_sequence_files/t0009-NEL.text rename to fixtures/escape_sequence_files/t0009-NEL.text diff --git a/src/test/escape_sequence_files/t0010-RI.in b/fixtures/escape_sequence_files/t0010-RI.in similarity index 100% rename from src/test/escape_sequence_files/t0010-RI.in rename to fixtures/escape_sequence_files/t0010-RI.in diff --git a/src/test/escape_sequence_files/t0010-RI.text b/fixtures/escape_sequence_files/t0010-RI.text similarity index 100% rename from src/test/escape_sequence_files/t0010-RI.text rename to fixtures/escape_sequence_files/t0010-RI.text diff --git a/src/test/escape_sequence_files/t0011-RI_scroll.in b/fixtures/escape_sequence_files/t0011-RI_scroll.in similarity index 100% rename from src/test/escape_sequence_files/t0011-RI_scroll.in rename to fixtures/escape_sequence_files/t0011-RI_scroll.in diff --git a/src/test/escape_sequence_files/t0011-RI_scroll.text b/fixtures/escape_sequence_files/t0011-RI_scroll.text similarity index 100% rename from src/test/escape_sequence_files/t0011-RI_scroll.text rename to fixtures/escape_sequence_files/t0011-RI_scroll.text diff --git a/src/test/escape_sequence_files/t0012-VT.in b/fixtures/escape_sequence_files/t0012-VT.in similarity index 100% rename from src/test/escape_sequence_files/t0012-VT.in rename to fixtures/escape_sequence_files/t0012-VT.in diff --git a/src/test/escape_sequence_files/t0012-VT.text b/fixtures/escape_sequence_files/t0012-VT.text similarity index 100% rename from src/test/escape_sequence_files/t0012-VT.text rename to fixtures/escape_sequence_files/t0012-VT.text diff --git a/src/test/escape_sequence_files/t0013-FF.in b/fixtures/escape_sequence_files/t0013-FF.in similarity index 100% rename from src/test/escape_sequence_files/t0013-FF.in rename to fixtures/escape_sequence_files/t0013-FF.in diff --git a/src/test/escape_sequence_files/t0013-FF.text b/fixtures/escape_sequence_files/t0013-FF.text similarity index 100% rename from src/test/escape_sequence_files/t0013-FF.text rename to fixtures/escape_sequence_files/t0013-FF.text diff --git a/src/test/escape_sequence_files/t0014-CAN.in b/fixtures/escape_sequence_files/t0014-CAN.in similarity index 100% rename from src/test/escape_sequence_files/t0014-CAN.in rename to fixtures/escape_sequence_files/t0014-CAN.in diff --git a/src/test/escape_sequence_files/t0014-CAN.text b/fixtures/escape_sequence_files/t0014-CAN.text similarity index 100% rename from src/test/escape_sequence_files/t0014-CAN.text rename to fixtures/escape_sequence_files/t0014-CAN.text diff --git a/src/test/escape_sequence_files/t0015-SUB.in b/fixtures/escape_sequence_files/t0015-SUB.in similarity index 100% rename from src/test/escape_sequence_files/t0015-SUB.in rename to fixtures/escape_sequence_files/t0015-SUB.in diff --git a/src/test/escape_sequence_files/t0015-SUB.text b/fixtures/escape_sequence_files/t0015-SUB.text similarity index 100% rename from src/test/escape_sequence_files/t0015-SUB.text rename to fixtures/escape_sequence_files/t0015-SUB.text diff --git a/src/test/escape_sequence_files/t0016-SU.in b/fixtures/escape_sequence_files/t0016-SU.in similarity index 100% rename from src/test/escape_sequence_files/t0016-SU.in rename to fixtures/escape_sequence_files/t0016-SU.in diff --git a/src/test/escape_sequence_files/t0016-SU.text b/fixtures/escape_sequence_files/t0016-SU.text similarity index 100% rename from src/test/escape_sequence_files/t0016-SU.text rename to fixtures/escape_sequence_files/t0016-SU.text diff --git a/src/test/escape_sequence_files/t0017-SD.in b/fixtures/escape_sequence_files/t0017-SD.in similarity index 100% rename from src/test/escape_sequence_files/t0017-SD.in rename to fixtures/escape_sequence_files/t0017-SD.in diff --git a/src/test/escape_sequence_files/t0017-SD.text b/fixtures/escape_sequence_files/t0017-SD.text similarity index 100% rename from src/test/escape_sequence_files/t0017-SD.text rename to fixtures/escape_sequence_files/t0017-SD.text diff --git a/src/test/escape_sequence_files/t0020-CUF.in b/fixtures/escape_sequence_files/t0020-CUF.in similarity index 100% rename from src/test/escape_sequence_files/t0020-CUF.in rename to fixtures/escape_sequence_files/t0020-CUF.in diff --git a/src/test/escape_sequence_files/t0020-CUF.text b/fixtures/escape_sequence_files/t0020-CUF.text similarity index 100% rename from src/test/escape_sequence_files/t0020-CUF.text rename to fixtures/escape_sequence_files/t0020-CUF.text diff --git a/src/test/escape_sequence_files/t0021-CUB.in b/fixtures/escape_sequence_files/t0021-CUB.in similarity index 100% rename from src/test/escape_sequence_files/t0021-CUB.in rename to fixtures/escape_sequence_files/t0021-CUB.in diff --git a/src/test/escape_sequence_files/t0021-CUB.text b/fixtures/escape_sequence_files/t0021-CUB.text similarity index 100% rename from src/test/escape_sequence_files/t0021-CUB.text rename to fixtures/escape_sequence_files/t0021-CUB.text diff --git a/src/test/escape_sequence_files/t0022-CUU.in b/fixtures/escape_sequence_files/t0022-CUU.in similarity index 100% rename from src/test/escape_sequence_files/t0022-CUU.in rename to fixtures/escape_sequence_files/t0022-CUU.in diff --git a/src/test/escape_sequence_files/t0022-CUU.text b/fixtures/escape_sequence_files/t0022-CUU.text similarity index 100% rename from src/test/escape_sequence_files/t0022-CUU.text rename to fixtures/escape_sequence_files/t0022-CUU.text diff --git a/src/test/escape_sequence_files/t0023-CUU_scroll.in b/fixtures/escape_sequence_files/t0023-CUU_scroll.in similarity index 100% rename from src/test/escape_sequence_files/t0023-CUU_scroll.in rename to fixtures/escape_sequence_files/t0023-CUU_scroll.in diff --git a/src/test/escape_sequence_files/t0023-CUU_scroll.text b/fixtures/escape_sequence_files/t0023-CUU_scroll.text similarity index 100% rename from src/test/escape_sequence_files/t0023-CUU_scroll.text rename to fixtures/escape_sequence_files/t0023-CUU_scroll.text diff --git a/src/test/escape_sequence_files/t0024-CUD.in b/fixtures/escape_sequence_files/t0024-CUD.in similarity index 100% rename from src/test/escape_sequence_files/t0024-CUD.in rename to fixtures/escape_sequence_files/t0024-CUD.in diff --git a/src/test/escape_sequence_files/t0024-CUD.text b/fixtures/escape_sequence_files/t0024-CUD.text similarity index 100% rename from src/test/escape_sequence_files/t0024-CUD.text rename to fixtures/escape_sequence_files/t0024-CUD.text diff --git a/src/test/escape_sequence_files/t0025-CUP.in b/fixtures/escape_sequence_files/t0025-CUP.in similarity index 100% rename from src/test/escape_sequence_files/t0025-CUP.in rename to fixtures/escape_sequence_files/t0025-CUP.in diff --git a/src/test/escape_sequence_files/t0025-CUP.text b/fixtures/escape_sequence_files/t0025-CUP.text similarity index 100% rename from src/test/escape_sequence_files/t0025-CUP.text rename to fixtures/escape_sequence_files/t0025-CUP.text diff --git a/src/test/escape_sequence_files/t0026-CNL.in b/fixtures/escape_sequence_files/t0026-CNL.in similarity index 100% rename from src/test/escape_sequence_files/t0026-CNL.in rename to fixtures/escape_sequence_files/t0026-CNL.in diff --git a/src/test/escape_sequence_files/t0026-CNL.text b/fixtures/escape_sequence_files/t0026-CNL.text similarity index 100% rename from src/test/escape_sequence_files/t0026-CNL.text rename to fixtures/escape_sequence_files/t0026-CNL.text diff --git a/src/test/escape_sequence_files/t0027-CPL.in b/fixtures/escape_sequence_files/t0027-CPL.in similarity index 100% rename from src/test/escape_sequence_files/t0027-CPL.in rename to fixtures/escape_sequence_files/t0027-CPL.in diff --git a/src/test/escape_sequence_files/t0027-CPL.text b/fixtures/escape_sequence_files/t0027-CPL.text similarity index 100% rename from src/test/escape_sequence_files/t0027-CPL.text rename to fixtures/escape_sequence_files/t0027-CPL.text diff --git a/src/test/escape_sequence_files/t0030-HPR.in b/fixtures/escape_sequence_files/t0030-HPR.in similarity index 100% rename from src/test/escape_sequence_files/t0030-HPR.in rename to fixtures/escape_sequence_files/t0030-HPR.in diff --git a/src/test/escape_sequence_files/t0030-HPR.text b/fixtures/escape_sequence_files/t0030-HPR.text similarity index 100% rename from src/test/escape_sequence_files/t0030-HPR.text rename to fixtures/escape_sequence_files/t0030-HPR.text diff --git a/src/test/escape_sequence_files/t0031-HPB.in_ b/fixtures/escape_sequence_files/t0031-HPB.in_ similarity index 100% rename from src/test/escape_sequence_files/t0031-HPB.in_ rename to fixtures/escape_sequence_files/t0031-HPB.in_ diff --git a/src/test/escape_sequence_files/t0031-HPB.text b/fixtures/escape_sequence_files/t0031-HPB.text similarity index 100% rename from src/test/escape_sequence_files/t0031-HPB.text rename to fixtures/escape_sequence_files/t0031-HPB.text diff --git a/src/test/escape_sequence_files/t0032-VPB.in b/fixtures/escape_sequence_files/t0032-VPB.in similarity index 100% rename from src/test/escape_sequence_files/t0032-VPB.in rename to fixtures/escape_sequence_files/t0032-VPB.in diff --git a/src/test/escape_sequence_files/t0032-VPB.text b/fixtures/escape_sequence_files/t0032-VPB.text similarity index 100% rename from src/test/escape_sequence_files/t0032-VPB.text rename to fixtures/escape_sequence_files/t0032-VPB.text diff --git a/src/test/escape_sequence_files/t0033-VPB_scroll.in b/fixtures/escape_sequence_files/t0033-VPB_scroll.in similarity index 100% rename from src/test/escape_sequence_files/t0033-VPB_scroll.in rename to fixtures/escape_sequence_files/t0033-VPB_scroll.in diff --git a/src/test/escape_sequence_files/t0033-VPB_scroll.text b/fixtures/escape_sequence_files/t0033-VPB_scroll.text similarity index 100% rename from src/test/escape_sequence_files/t0033-VPB_scroll.text rename to fixtures/escape_sequence_files/t0033-VPB_scroll.text diff --git a/src/test/escape_sequence_files/t0034-VPR.in b/fixtures/escape_sequence_files/t0034-VPR.in similarity index 100% rename from src/test/escape_sequence_files/t0034-VPR.in rename to fixtures/escape_sequence_files/t0034-VPR.in diff --git a/src/test/escape_sequence_files/t0034-VPR.text b/fixtures/escape_sequence_files/t0034-VPR.text similarity index 100% rename from src/test/escape_sequence_files/t0034-VPR.text rename to fixtures/escape_sequence_files/t0034-VPR.text diff --git a/src/test/escape_sequence_files/t0035-HVP.in b/fixtures/escape_sequence_files/t0035-HVP.in similarity index 100% rename from src/test/escape_sequence_files/t0035-HVP.in rename to fixtures/escape_sequence_files/t0035-HVP.in diff --git a/src/test/escape_sequence_files/t0035-HVP.text b/fixtures/escape_sequence_files/t0035-HVP.text similarity index 100% rename from src/test/escape_sequence_files/t0035-HVP.text rename to fixtures/escape_sequence_files/t0035-HVP.text diff --git a/src/test/escape_sequence_files/t0040-REP.in b/fixtures/escape_sequence_files/t0040-REP.in similarity index 100% rename from src/test/escape_sequence_files/t0040-REP.in rename to fixtures/escape_sequence_files/t0040-REP.in diff --git a/src/test/escape_sequence_files/t0040-REP.text b/fixtures/escape_sequence_files/t0040-REP.text similarity index 100% rename from src/test/escape_sequence_files/t0040-REP.text rename to fixtures/escape_sequence_files/t0040-REP.text diff --git a/src/test/escape_sequence_files/t0050-ICH.in b/fixtures/escape_sequence_files/t0050-ICH.in similarity index 100% rename from src/test/escape_sequence_files/t0050-ICH.in rename to fixtures/escape_sequence_files/t0050-ICH.in diff --git a/src/test/escape_sequence_files/t0050-ICH.text b/fixtures/escape_sequence_files/t0050-ICH.text similarity index 100% rename from src/test/escape_sequence_files/t0050-ICH.text rename to fixtures/escape_sequence_files/t0050-ICH.text diff --git a/src/test/escape_sequence_files/t0051-IL.in b/fixtures/escape_sequence_files/t0051-IL.in similarity index 100% rename from src/test/escape_sequence_files/t0051-IL.in rename to fixtures/escape_sequence_files/t0051-IL.in diff --git a/src/test/escape_sequence_files/t0051-IL.text b/fixtures/escape_sequence_files/t0051-IL.text similarity index 100% rename from src/test/escape_sequence_files/t0051-IL.text rename to fixtures/escape_sequence_files/t0051-IL.text diff --git a/src/test/escape_sequence_files/t0052-DL.in b/fixtures/escape_sequence_files/t0052-DL.in similarity index 100% rename from src/test/escape_sequence_files/t0052-DL.in rename to fixtures/escape_sequence_files/t0052-DL.in diff --git a/src/test/escape_sequence_files/t0052-DL.text b/fixtures/escape_sequence_files/t0052-DL.text similarity index 100% rename from src/test/escape_sequence_files/t0052-DL.text rename to fixtures/escape_sequence_files/t0052-DL.text diff --git a/src/test/escape_sequence_files/t0053-DCH.in b/fixtures/escape_sequence_files/t0053-DCH.in similarity index 100% rename from src/test/escape_sequence_files/t0053-DCH.in rename to fixtures/escape_sequence_files/t0053-DCH.in diff --git a/src/test/escape_sequence_files/t0053-DCH.text b/fixtures/escape_sequence_files/t0053-DCH.text similarity index 100% rename from src/test/escape_sequence_files/t0053-DCH.text rename to fixtures/escape_sequence_files/t0053-DCH.text diff --git a/src/test/escape_sequence_files/t0054-ECH.in b/fixtures/escape_sequence_files/t0054-ECH.in similarity index 100% rename from src/test/escape_sequence_files/t0054-ECH.in rename to fixtures/escape_sequence_files/t0054-ECH.in diff --git a/src/test/escape_sequence_files/t0054-ECH.text b/fixtures/escape_sequence_files/t0054-ECH.text similarity index 100% rename from src/test/escape_sequence_files/t0054-ECH.text rename to fixtures/escape_sequence_files/t0054-ECH.text diff --git a/src/test/escape_sequence_files/t0055-EL.in b/fixtures/escape_sequence_files/t0055-EL.in similarity index 100% rename from src/test/escape_sequence_files/t0055-EL.in rename to fixtures/escape_sequence_files/t0055-EL.in diff --git a/src/test/escape_sequence_files/t0055-EL.text b/fixtures/escape_sequence_files/t0055-EL.text similarity index 100% rename from src/test/escape_sequence_files/t0055-EL.text rename to fixtures/escape_sequence_files/t0055-EL.text diff --git a/src/test/escape_sequence_files/t0056-ED.in b/fixtures/escape_sequence_files/t0056-ED.in similarity index 100% rename from src/test/escape_sequence_files/t0056-ED.in rename to fixtures/escape_sequence_files/t0056-ED.in diff --git a/src/test/escape_sequence_files/t0056-ED.text b/fixtures/escape_sequence_files/t0056-ED.text similarity index 100% rename from src/test/escape_sequence_files/t0056-ED.text rename to fixtures/escape_sequence_files/t0056-ED.text diff --git a/src/test/escape_sequence_files/t0057-ED3.in b/fixtures/escape_sequence_files/t0057-ED3.in similarity index 100% rename from src/test/escape_sequence_files/t0057-ED3.in rename to fixtures/escape_sequence_files/t0057-ED3.in diff --git a/src/test/escape_sequence_files/t0057-ED3.note b/fixtures/escape_sequence_files/t0057-ED3.note similarity index 100% rename from src/test/escape_sequence_files/t0057-ED3.note rename to fixtures/escape_sequence_files/t0057-ED3.note diff --git a/src/test/escape_sequence_files/t0057-ED3.text b/fixtures/escape_sequence_files/t0057-ED3.text similarity index 100% rename from src/test/escape_sequence_files/t0057-ED3.text rename to fixtures/escape_sequence_files/t0057-ED3.text diff --git a/src/test/escape_sequence_files/t0060-DECSC.in b/fixtures/escape_sequence_files/t0060-DECSC.in similarity index 100% rename from src/test/escape_sequence_files/t0060-DECSC.in rename to fixtures/escape_sequence_files/t0060-DECSC.in diff --git a/src/test/escape_sequence_files/t0060-DECSC.text b/fixtures/escape_sequence_files/t0060-DECSC.text similarity index 100% rename from src/test/escape_sequence_files/t0060-DECSC.text rename to fixtures/escape_sequence_files/t0060-DECSC.text diff --git a/src/test/escape_sequence_files/t0061-CSI_s.in b/fixtures/escape_sequence_files/t0061-CSI_s.in similarity index 100% rename from src/test/escape_sequence_files/t0061-CSI_s.in rename to fixtures/escape_sequence_files/t0061-CSI_s.in diff --git a/src/test/escape_sequence_files/t0061-CSI_s.text b/fixtures/escape_sequence_files/t0061-CSI_s.text similarity index 100% rename from src/test/escape_sequence_files/t0061-CSI_s.text rename to fixtures/escape_sequence_files/t0061-CSI_s.text diff --git a/src/test/escape_sequence_files/t0070-DECSTBM_LF.in b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in similarity index 100% rename from src/test/escape_sequence_files/t0070-DECSTBM_LF.in rename to fixtures/escape_sequence_files/t0070-DECSTBM_LF.in diff --git a/src/test/escape_sequence_files/t0070-DECSTBM_LF.text b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text similarity index 100% rename from src/test/escape_sequence_files/t0070-DECSTBM_LF.text rename to fixtures/escape_sequence_files/t0070-DECSTBM_LF.text diff --git a/src/test/escape_sequence_files/t0071-DECSTBM_IND.in b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in similarity index 100% rename from src/test/escape_sequence_files/t0071-DECSTBM_IND.in rename to fixtures/escape_sequence_files/t0071-DECSTBM_IND.in diff --git a/src/test/escape_sequence_files/t0071-DECSTBM_IND.text b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text similarity index 100% rename from src/test/escape_sequence_files/t0071-DECSTBM_IND.text rename to fixtures/escape_sequence_files/t0071-DECSTBM_IND.text diff --git a/src/test/escape_sequence_files/t0072-DECSTBM_NEL.in b/fixtures/escape_sequence_files/t0072-DECSTBM_NEL.in similarity index 100% rename from src/test/escape_sequence_files/t0072-DECSTBM_NEL.in rename to fixtures/escape_sequence_files/t0072-DECSTBM_NEL.in diff --git a/src/test/escape_sequence_files/t0072-DECSTBM_NEL.text b/fixtures/escape_sequence_files/t0072-DECSTBM_NEL.text similarity index 100% rename from src/test/escape_sequence_files/t0072-DECSTBM_NEL.text rename to fixtures/escape_sequence_files/t0072-DECSTBM_NEL.text diff --git a/src/test/escape_sequence_files/t0073-DECSTBM_RI.in b/fixtures/escape_sequence_files/t0073-DECSTBM_RI.in similarity index 100% rename from src/test/escape_sequence_files/t0073-DECSTBM_RI.in rename to fixtures/escape_sequence_files/t0073-DECSTBM_RI.in diff --git a/src/test/escape_sequence_files/t0073-DECSTBM_RI.text b/fixtures/escape_sequence_files/t0073-DECSTBM_RI.text similarity index 100% rename from src/test/escape_sequence_files/t0073-DECSTBM_RI.text rename to fixtures/escape_sequence_files/t0073-DECSTBM_RI.text diff --git a/src/test/escape_sequence_files/t0074-DECSTBM_SU_SD.in b/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.in similarity index 100% rename from src/test/escape_sequence_files/t0074-DECSTBM_SU_SD.in rename to fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.in diff --git a/src/test/escape_sequence_files/t0074-DECSTBM_SU_SD.text b/fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text similarity index 100% rename from src/test/escape_sequence_files/t0074-DECSTBM_SU_SD.text rename to fixtures/escape_sequence_files/t0074-DECSTBM_SU_SD.text diff --git a/src/test/escape_sequence_files/t0075-DECSTBM_CUU_CUD.in b/fixtures/escape_sequence_files/t0075-DECSTBM_CUU_CUD.in similarity index 100% rename from src/test/escape_sequence_files/t0075-DECSTBM_CUU_CUD.in rename to fixtures/escape_sequence_files/t0075-DECSTBM_CUU_CUD.in diff --git a/src/test/escape_sequence_files/t0075-DECSTBM_CUU_CUD.text b/fixtures/escape_sequence_files/t0075-DECSTBM_CUU_CUD.text similarity index 100% rename from src/test/escape_sequence_files/t0075-DECSTBM_CUU_CUD.text rename to fixtures/escape_sequence_files/t0075-DECSTBM_CUU_CUD.text diff --git a/src/test/escape_sequence_files/t0076-DECSTBM_IL_DL.in b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.in similarity index 100% rename from src/test/escape_sequence_files/t0076-DECSTBM_IL_DL.in rename to fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.in diff --git a/src/test/escape_sequence_files/t0076-DECSTBM_IL_DL.text b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text similarity index 100% rename from src/test/escape_sequence_files/t0076-DECSTBM_IL_DL.text rename to fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text diff --git a/src/test/escape_sequence_files/t0077-DECSTBM_quirks.in b/fixtures/escape_sequence_files/t0077-DECSTBM_quirks.in similarity index 100% rename from src/test/escape_sequence_files/t0077-DECSTBM_quirks.in rename to fixtures/escape_sequence_files/t0077-DECSTBM_quirks.in diff --git a/src/test/escape_sequence_files/t0077-DECSTBM_quirks.text b/fixtures/escape_sequence_files/t0077-DECSTBM_quirks.text similarity index 100% rename from src/test/escape_sequence_files/t0077-DECSTBM_quirks.text rename to fixtures/escape_sequence_files/t0077-DECSTBM_quirks.text diff --git a/src/test/escape_sequence_files/t0080-HT.in b/fixtures/escape_sequence_files/t0080-HT.in similarity index 100% rename from src/test/escape_sequence_files/t0080-HT.in rename to fixtures/escape_sequence_files/t0080-HT.in diff --git a/src/test/escape_sequence_files/t0080-HT.text b/fixtures/escape_sequence_files/t0080-HT.text similarity index 100% rename from src/test/escape_sequence_files/t0080-HT.text rename to fixtures/escape_sequence_files/t0080-HT.text diff --git a/src/test/escape_sequence_files/t0081-TBC.in b/fixtures/escape_sequence_files/t0081-TBC.in similarity index 100% rename from src/test/escape_sequence_files/t0081-TBC.in rename to fixtures/escape_sequence_files/t0081-TBC.in diff --git a/src/test/escape_sequence_files/t0081-TBC.text b/fixtures/escape_sequence_files/t0081-TBC.text similarity index 100% rename from src/test/escape_sequence_files/t0081-TBC.text rename to fixtures/escape_sequence_files/t0081-TBC.text diff --git a/src/test/escape_sequence_files/t0082-HTS.in b/fixtures/escape_sequence_files/t0082-HTS.in similarity index 100% rename from src/test/escape_sequence_files/t0082-HTS.in rename to fixtures/escape_sequence_files/t0082-HTS.in diff --git a/src/test/escape_sequence_files/t0082-HTS.text b/fixtures/escape_sequence_files/t0082-HTS.text similarity index 100% rename from src/test/escape_sequence_files/t0082-HTS.text rename to fixtures/escape_sequence_files/t0082-HTS.text diff --git a/src/test/escape_sequence_files/t0083-CHT.in b/fixtures/escape_sequence_files/t0083-CHT.in similarity index 100% rename from src/test/escape_sequence_files/t0083-CHT.in rename to fixtures/escape_sequence_files/t0083-CHT.in diff --git a/src/test/escape_sequence_files/t0083-CHT.text b/fixtures/escape_sequence_files/t0083-CHT.text similarity index 100% rename from src/test/escape_sequence_files/t0083-CHT.text rename to fixtures/escape_sequence_files/t0083-CHT.text diff --git a/src/test/escape_sequence_files/t0084-CBT.in b/fixtures/escape_sequence_files/t0084-CBT.in similarity index 100% rename from src/test/escape_sequence_files/t0084-CBT.in rename to fixtures/escape_sequence_files/t0084-CBT.in diff --git a/src/test/escape_sequence_files/t0084-CBT.text b/fixtures/escape_sequence_files/t0084-CBT.text similarity index 100% rename from src/test/escape_sequence_files/t0084-CBT.text rename to fixtures/escape_sequence_files/t0084-CBT.text diff --git a/src/test/escape_sequence_files/t0084-CBT.text-xterm b/fixtures/escape_sequence_files/t0084-CBT.text-xterm similarity index 100% rename from src/test/escape_sequence_files/t0084-CBT.text-xterm rename to fixtures/escape_sequence_files/t0084-CBT.text-xterm diff --git a/src/test/escape_sequence_files/t0090-alt_screen.in b/fixtures/escape_sequence_files/t0090-alt_screen.in similarity index 100% rename from src/test/escape_sequence_files/t0090-alt_screen.in rename to fixtures/escape_sequence_files/t0090-alt_screen.in diff --git a/src/test/escape_sequence_files/t0090-alt_screen.text b/fixtures/escape_sequence_files/t0090-alt_screen.text similarity index 100% rename from src/test/escape_sequence_files/t0090-alt_screen.text rename to fixtures/escape_sequence_files/t0090-alt_screen.text diff --git a/src/test/escape_sequence_files/t0091-alt_screen_ED3.in b/fixtures/escape_sequence_files/t0091-alt_screen_ED3.in similarity index 100% rename from src/test/escape_sequence_files/t0091-alt_screen_ED3.in rename to fixtures/escape_sequence_files/t0091-alt_screen_ED3.in diff --git a/src/test/escape_sequence_files/t0091-alt_screen_ED3.text b/fixtures/escape_sequence_files/t0091-alt_screen_ED3.text similarity index 100% rename from src/test/escape_sequence_files/t0091-alt_screen_ED3.text rename to fixtures/escape_sequence_files/t0091-alt_screen_ED3.text diff --git a/src/test/escape_sequence_files/t0092-alt_screen_DECSC.in b/fixtures/escape_sequence_files/t0092-alt_screen_DECSC.in similarity index 100% rename from src/test/escape_sequence_files/t0092-alt_screen_DECSC.in rename to fixtures/escape_sequence_files/t0092-alt_screen_DECSC.in diff --git a/src/test/escape_sequence_files/t0092-alt_screen_DECSC.text b/fixtures/escape_sequence_files/t0092-alt_screen_DECSC.text similarity index 100% rename from src/test/escape_sequence_files/t0092-alt_screen_DECSC.text rename to fixtures/escape_sequence_files/t0092-alt_screen_DECSC.text diff --git a/src/test/escape_sequence_files/t0100-IRM.in b/fixtures/escape_sequence_files/t0100-IRM.in similarity index 100% rename from src/test/escape_sequence_files/t0100-IRM.in rename to fixtures/escape_sequence_files/t0100-IRM.in diff --git a/src/test/escape_sequence_files/t0100-IRM.text b/fixtures/escape_sequence_files/t0100-IRM.text similarity index 100% rename from src/test/escape_sequence_files/t0100-IRM.text rename to fixtures/escape_sequence_files/t0100-IRM.text diff --git a/src/test/escape_sequence_files/t0101-NLM.in b/fixtures/escape_sequence_files/t0101-NLM.in similarity index 100% rename from src/test/escape_sequence_files/t0101-NLM.in rename to fixtures/escape_sequence_files/t0101-NLM.in diff --git a/src/test/escape_sequence_files/t0101-NLM.text b/fixtures/escape_sequence_files/t0101-NLM.text similarity index 100% rename from src/test/escape_sequence_files/t0101-NLM.text rename to fixtures/escape_sequence_files/t0101-NLM.text diff --git a/src/test/escape_sequence_files/t0102-DECAWM.in b/fixtures/escape_sequence_files/t0102-DECAWM.in similarity index 100% rename from src/test/escape_sequence_files/t0102-DECAWM.in rename to fixtures/escape_sequence_files/t0102-DECAWM.in diff --git a/src/test/escape_sequence_files/t0102-DECAWM.text b/fixtures/escape_sequence_files/t0102-DECAWM.text similarity index 100% rename from src/test/escape_sequence_files/t0102-DECAWM.text rename to fixtures/escape_sequence_files/t0102-DECAWM.text diff --git a/src/test/escape_sequence_files/t0103-reverse_wrap.in b/fixtures/escape_sequence_files/t0103-reverse_wrap.in similarity index 100% rename from src/test/escape_sequence_files/t0103-reverse_wrap.in rename to fixtures/escape_sequence_files/t0103-reverse_wrap.in diff --git a/src/test/escape_sequence_files/t0103-reverse_wrap.text b/fixtures/escape_sequence_files/t0103-reverse_wrap.text similarity index 100% rename from src/test/escape_sequence_files/t0103-reverse_wrap.text rename to fixtures/escape_sequence_files/t0103-reverse_wrap.text diff --git a/src/test/escape_sequence_files/t0200-SGR.html b/fixtures/escape_sequence_files/t0200-SGR.html similarity index 100% rename from src/test/escape_sequence_files/t0200-SGR.html rename to fixtures/escape_sequence_files/t0200-SGR.html diff --git a/src/test/escape_sequence_files/t0200-SGR.in_ b/fixtures/escape_sequence_files/t0200-SGR.in_ similarity index 100% rename from src/test/escape_sequence_files/t0200-SGR.in_ rename to fixtures/escape_sequence_files/t0200-SGR.in_ diff --git a/src/test/escape_sequence_files/t0220-SGR_inverse.html b/fixtures/escape_sequence_files/t0220-SGR_inverse.html similarity index 100% rename from src/test/escape_sequence_files/t0220-SGR_inverse.html rename to fixtures/escape_sequence_files/t0220-SGR_inverse.html diff --git a/src/test/escape_sequence_files/t0220-SGR_inverse.in_ b/fixtures/escape_sequence_files/t0220-SGR_inverse.in_ similarity index 100% rename from src/test/escape_sequence_files/t0220-SGR_inverse.in_ rename to fixtures/escape_sequence_files/t0220-SGR_inverse.in_ diff --git a/src/test/escape_sequence_files/t0500-bash_long_line.in b/fixtures/escape_sequence_files/t0500-bash_long_line.in similarity index 100% rename from src/test/escape_sequence_files/t0500-bash_long_line.in rename to fixtures/escape_sequence_files/t0500-bash_long_line.in diff --git a/src/test/escape_sequence_files/t0500-bash_long_line.text b/fixtures/escape_sequence_files/t0500-bash_long_line.text similarity index 100% rename from src/test/escape_sequence_files/t0500-bash_long_line.text rename to fixtures/escape_sequence_files/t0500-bash_long_line.text diff --git a/src/test/escape_sequence_files/t0501-bash_ls.in b/fixtures/escape_sequence_files/t0501-bash_ls.in similarity index 100% rename from src/test/escape_sequence_files/t0501-bash_ls.in rename to fixtures/escape_sequence_files/t0501-bash_ls.in diff --git a/src/test/escape_sequence_files/t0501-bash_ls.text b/fixtures/escape_sequence_files/t0501-bash_ls.text similarity index 100% rename from src/test/escape_sequence_files/t0501-bash_ls.text rename to fixtures/escape_sequence_files/t0501-bash_ls.text diff --git a/src/test/escape_sequence_files/t0502-bash_ls_color.in b/fixtures/escape_sequence_files/t0502-bash_ls_color.in similarity index 100% rename from src/test/escape_sequence_files/t0502-bash_ls_color.in rename to fixtures/escape_sequence_files/t0502-bash_ls_color.in diff --git a/src/test/escape_sequence_files/t0502-bash_ls_color.text b/fixtures/escape_sequence_files/t0502-bash_ls_color.text similarity index 100% rename from src/test/escape_sequence_files/t0502-bash_ls_color.text rename to fixtures/escape_sequence_files/t0502-bash_ls_color.text diff --git a/src/test/escape_sequence_files/t0503-zsh_ls_color.in b/fixtures/escape_sequence_files/t0503-zsh_ls_color.in similarity index 100% rename from src/test/escape_sequence_files/t0503-zsh_ls_color.in rename to fixtures/escape_sequence_files/t0503-zsh_ls_color.in diff --git a/src/test/escape_sequence_files/t0503-zsh_ls_color.text b/fixtures/escape_sequence_files/t0503-zsh_ls_color.text similarity index 100% rename from src/test/escape_sequence_files/t0503-zsh_ls_color.text rename to fixtures/escape_sequence_files/t0503-zsh_ls_color.text diff --git a/src/test/escape_sequence_files/t0504-vim.in b/fixtures/escape_sequence_files/t0504-vim.in similarity index 100% rename from src/test/escape_sequence_files/t0504-vim.in rename to fixtures/escape_sequence_files/t0504-vim.in diff --git a/src/test/escape_sequence_files/t0504-vim.text b/fixtures/escape_sequence_files/t0504-vim.text similarity index 100% rename from src/test/escape_sequence_files/t0504-vim.text rename to fixtures/escape_sequence_files/t0504-vim.text diff --git a/gulpfile.js b/gulpfile.js index 6e211023..8469c66c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -121,7 +121,12 @@ gulp.task('instrument-test', function () { }); gulp.task('mocha', ['instrument-test'], function () { - return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false}) + return gulp.src([ + `${outDir}/*test.js`, + `${outDir}/**/*test.js`, + `${outDir}/*integration.js`, + `${outDir}/**/*integration.js` + ], {read: false}) .pipe(mocha()) .once('error', () => process.exit(1)) .pipe(istanbul.writeReports()); diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 99bd6d0f..5ac71996 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { Buffer } from './Buffer'; import { CircularList } from './utils/CircularList'; -import { MockTerminal } from './utils/TestUtils'; +import { MockTerminal } from './utils/TestUtils.test'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 97c07995..320241ca 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { BufferSet } from './BufferSet'; import { Buffer } from './Buffer'; -import { MockTerminal } from './utils/TestUtils'; +import { MockTerminal } from './utils/TestUtils.test'; describe('BufferSet', () => { let terminal: ITerminal; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 043b7919..f6e07f7e 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { InputHandler } from './InputHandler'; import { wcwidth } from './InputHandler'; -import { MockInputHandlingTerminal } from './utils/TestUtils'; +import { MockInputHandlingTerminal } from './utils/TestUtils.test'; describe('InputHandler', () => { describe('save and restore cursor', () => { diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f97d9294..f4ae917f 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -158,6 +158,9 @@ export interface IBufferSet { export interface IViewport { syncScrollArea(): void; + onWheel(ev: WheelEvent): void; + onTouchStart(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): void; } export interface ISelectionManager { @@ -171,6 +174,14 @@ export interface ISelectionManager { setSelection(row: number, col: number, length: number): void; } +export interface ICompositionHelper { + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + export interface ICharMeasure { width: number; height: number; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c7fd9aef..4774b3bb 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,7 +10,7 @@ import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { MockTerminal } from './utils/TestUtils'; +import { MockTerminal } from './utils/TestUtils.test'; import { LineData } from './Types'; class TestSelectionManager extends SelectionManager { diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 9da84478..6c750ed2 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { SelectionModel } from './SelectionModel'; import {BufferSet} from './BufferSet'; -import { MockTerminal } from './utils/TestUtils'; +import { MockTerminal } from './utils/TestUtils.test'; class TestSelectionModel extends SelectionModel { constructor( diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts new file mode 100644 index 00000000..2155d0e0 --- /dev/null +++ b/src/Terminal.integration.ts @@ -0,0 +1,145 @@ +/** + * @license MIT + * + * This file contains integration tests for xterm.js. + */ + +import * as glob from 'glob'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as pty from 'node-pty'; +import { Terminal } from './Terminal'; +import { CHAR_DATA_CHAR_INDEX } from './Buffer'; + +let primitive_pty: any; + +// fake sychronous pty write - read +// we just pipe the data from slave to master as a child program would do +// pty.js opens pipe fds with O_NONBLOCK +// just wait 10ms instead of setting fds to blocking mode +function ptyWriteRead(data: string, cb: (result: string) => void): void { + fs.writeSync(primitive_pty.slave, data); + setTimeout(() => { + let b = new Buffer(64000); + let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null); + cb(b.toString('utf8', 0, bytes)); + }); +} + +// make sure raw pty is at x=0 and has no pending data +function ptyReset(cb: (result: string) => void): void { + ptyWriteRead('\r\n', cb); +} + +/* debug helpers */ +// generate colorful noisy output to compare xterm and emulator cell states +function formatError(in_: string, out_: string, expected: string): string { + function addLineNumber(start: number, color: string): (s: string) => string { + let counter = start || 0; + return function(s: string): string { + counter += 1; + return '\x1b[33m' + (' ' + counter).slice(-2) + color + s; + }; + } + let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; + let s = ''; + s += '\n\x1b[34m' + JSON.stringify(in_); + s += '\n\x1b[33m ' + line80 + '\n'; + s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n'); + s += '\n\x1b[33m ' + line80 + '\n'; + s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n'); + return s; +} + +// simple debug output of terminal cells +function terminalToString(term: Terminal): string { + let result = ''; + let line_s = ''; + for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { + line_s = ''; + for (let cell = 0; cell < term.cols; ++cell) { + line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX]; + } + // rtrim empty cells as xterm does + line_s = line_s.replace(/\s+$/, ''); + result += line_s; + result += '\n'; + } + return result; +} + +// Skip tests on Windows since pty.open isn't supported +if (os.platform() !== 'win32') { + let CONSOLE_LOG = console.log; + + // expect files need terminal at 80x25! + let COLS = 80; + let ROWS = 25; + + /** some helpers for pty interaction */ + // we need a pty in between to get the termios decorations + // for the basic test cases a raw pty device is enough + primitive_pty = pty.native.open(COLS, ROWS); + + /** tests */ + describe('xterm output comparison', () => { + let xterm; + + beforeEach(() => { + xterm = new Terminal({ cols: COLS, rows: ROWS }); + xterm.refresh = () => {}; + xterm.viewport = { + syncScrollArea: () => {} + }; + }); + + // omit stack trace for escape sequence files + Error.stackTraceLimit = 0; + let files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); + // only successful tests for now + let skip = [ + 10, 16, 17, 19, 32, 33, 34, 35, 36, 39, + 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, + 63, 68 + ]; + if (os.platform() === 'darwin') { + // These are failing on macOS only + skip.push(3, 7, 11, 67); + } + for (let i = 0; i < files.length; i++) { + if (skip.indexOf(i) >= 0) { + continue; + } + ((filename: string) => { + it(filename.split('/').slice(-1)[0], done => { + ptyReset(() => { + let in_file = fs.readFileSync(filename, 'utf8'); + ptyWriteRead(in_file, from_pty => { + // uncomment this to get log from terminal + // console.log = function(){}; + + // Perform a synchronous .write(data) + xterm.writeBuffer.push(from_pty); + xterm.innerWrite(); + + let from_emulator = terminalToString(xterm); + console.log = CONSOLE_LOG; + let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); + // Some of the tests have whitespace on the right of lines, we trim all the linex + // from xterm.js so ignore this for now at least. + let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n'); + if (from_emulator !== expectedRightTrimmed) { + // uncomment to get noisy output + throw new Error(formatError(in_file, from_emulator, expected)); + // throw new Error('mismatch'); + } + done(); + }); + }); + }); + })(files[i]); + } + }); +} diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts new file mode 100644 index 00000000..f045a37e --- /dev/null +++ b/src/Terminal.test.ts @@ -0,0 +1,907 @@ +import { assert, expect } from 'chai'; +import { Terminal } from './Terminal'; +import { MockViewport, MockCompositionHelper } from './utils/TestUtils.test'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; + +class TestTerminal extends Terminal { + public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollDisp: number} { return this._evaluateKeyEscapeSequence(ev); } + public keyDown(ev: any): boolean { return this._keyDown(ev); } + public keyPress(ev: any): boolean { return this._keyPress(ev); } +} + +describe('term.js addons', () => { + let term: TestTerminal; + + beforeEach(() => { + term = new TestTerminal(); + term.refresh = () => {}; + term.viewport = new MockViewport(); + (term).compositionHelper = new MockCompositionHelper(); + // Force synchronous writes + term.write = (data) => { + term.writeBuffer.push(data); + (term).innerWrite(); + }; + (term).element = { + classList: { + toggle: () => {}, + remove: () => {} + } + }; + }); + + it('should load addons with Terminal.loadAddon', () => { + Terminal.loadAddon('attach'); + // Test that addon was loaded successfully, adding attach to Terminal's + // prototype. + assert.equal(typeof (Terminal).prototype.attach, 'function'); + }); + + describe('getOption', () => { + it('should retrieve the option correctly', () => { + // In the `options` namespace. + term.options.cursorBlink = true; + assert.equal(term.getOption('cursorBlink'), true); + + // On the Terminal instance + delete term.options.cursorBlink; + term.options.cursorBlink = false; + assert.equal(term.getOption('cursorBlink'), false); + }); + it('should throw when retrieving a non-existant option', () => { + assert.throws(term.getOption.bind(term, 'fake', true)); + }); + }); + + describe('attachCustomKeyEventHandler', () => { + let evKeyDown = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keydown', + keyCode: 77 + }; + let evKeyPress = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keypress', + keyCode: 77 + }; + + beforeEach(() => { + term.handler = () => {}; + term.showCursor = () => {}; + term.clearSelection = () => {}; + }); + + it('should process the keydown/keypress event based on what the handler returns', () => { + assert.equal(term.keyDown(evKeyDown), true); + assert.equal(term.keyPress(evKeyPress), true); + term.attachCustomKeyEventHandler(ev => ev.keyCode === 77); + assert.equal(term.keyDown(evKeyDown), true); + assert.equal(term.keyPress(evKeyPress), true); + term.attachCustomKeyEventHandler(ev => ev.keyCode !== 77); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + }); + + it('should alive after reset(ESC c Full Reset (RIS))', () => { + term.attachCustomKeyEventHandler(ev => ev.keyCode !== 77); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + term.reset(); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + }); + }); + + describe('setOption', () => { + let originalWarn; + let warnCallCount; + beforeEach(() => { + originalWarn = console.warn; + warnCallCount = 0; + console.warn = () => warnCallCount++; + }); + afterEach(() => { + console.warn = originalWarn; + }); + it('should set the option correctly', () => { + term.setOption('cursorBlink', true); + assert.equal(term.options.cursorBlink, true); + term.setOption('cursorBlink', false); + assert.equal(term.options.cursorBlink, false); + }); + it('should throw when setting a non-existant option', () => { + assert.throws(term.setOption.bind(term, 'fake', true)); + }); + it('should warn and do nothing when scrollback is less than number of rows', () => { + term.setOption('scrollback', term.rows - 1); + assert.equal(term.getOption('scrollback'), 1000); + assert.equal(warnCallCount, 1); + }); + }); + + describe('clear', () => { + it('should clear a buffer equal to rows', () => { + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + it('should clear a buffer larger than rows', () => { + // Fill the buffer with dummy rows + for (let i = 0; i < term.rows * 2; i++) { + term.write('test\n'); + } + + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + it('should not break the prompt when cleared twice', () => { + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + }); + + describe('scroll', () => { + describe('scrollDisp', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 2; i++) { + term.writeln('test'); + } + startYDisp = term.rows + 1; + }); + it('should scroll a single line', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, startYDisp - 1); + term.scrollDisp(1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should scroll multiple lines', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-5); + assert.equal(term.buffer.ydisp, startYDisp - 5); + term.scrollDisp(5); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should not scroll beyond the bounds of the buffer', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(1); + assert.equal(term.buffer.ydisp, startYDisp); + for (let i = 0; i < startYDisp; i++) { + term.scrollDisp(-1); + } + assert.equal(term.buffer.ydisp, 0); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, 0); + }); + }); + + describe('scrollPages', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll a single page', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1)); + term.scrollPages(1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should scroll a multiple pages', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-2); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1) * 2); + term.scrollPages(2); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('scrollToTop', () => { + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + }); + it('should scroll to the top', () => { + assert.notEqual(term.buffer.ydisp, 0); + term.scrollToTop(); + assert.equal(term.buffer.ydisp, 0); + }); + }); + + describe('scrollToBottom', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll to the bottom', () => { + term.scrollDisp(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToTop(); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('keyDown', () => { + it('should scroll down, when a key is pressed and terminal is scrolled up', () => { + // Override _evaluateKeyEscapeSequence to return cancel code + (term)._evaluateKeyEscapeSequence = () => { + return { key: 'a' }; + }; + let event = { + type: 'keydown', + keyCode: 0, + preventDefault: () => {}, + stopPropagation: () => {} + }; + + term.buffer.ydisp = 0; + term.buffer.ybase = 40; + term.keyDown(event); + + // Ensure that now the terminal is scrolled to bottom + assert.equal(term.buffer.ydisp, term.buffer.ybase); + }); + + it('should not scroll down, when a custom keydown handler prevents the event', () => { + // Add some output to the terminal + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + let startYDisp = (term.rows * 2) + 1; + term.attachCustomKeyEventHandler(() => { + return false; + }); + + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, startYDisp - 1); + term.keyDown({ keyCode: 0 }); + assert.equal(term.buffer.ydisp, startYDisp - 1); + }); + }); + }); + + describe('evaluateKeyEscapeSequence', () => { + it('should return the correct escape sequence for unmodified keys', () => { + // Backspace + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^? + // Tab + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t'); + // Return/enter + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR + // Escape + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b'); + // Page up, page down + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ + // End, Home + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H + // Left, up, right, down arrows + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B + // Insert + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~ + // Delete + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~ + // F1-F12 + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ + }); + it('should return \\x1b[3;5~ for ctrl+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~'); + }); + it('should return \\x1b[3;2~ for shift+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~'); + }); + it('should return \\x1b[3;3~ for alt+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~'); + }); + it('should return \\x1b[5D for ctrl+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D + }); + it('should return \\x1b[5C for ctrl+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C + }); + it('should return \\x1b[5A for ctrl+up', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A + }); + it('should return \\x1b[5B for ctrl+down', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B + }); + + describe('On non-macOS platforms', () => { + beforeEach(() => { + term.browser.isMac = false; + }); + // Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100 + // http://unix.stackexchange.com/a/108106 + it('should return \\x1b[5D for alt+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D + }); + it('should return \\x1b[5C for alt+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C + }); + }); + + describe('On macOS platforms', () => { + beforeEach(() => { + term.browser.isMac = true; + }); + it('should return \\x1bb for alt+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D + }); + it('should return \\x1bf for alt+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C + }); + }); + + it('should return \\x1b[5A for alt+up', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A + }); + it('should return \\x1b[5B for alt+down', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B + }); + it('should return the correct escape sequence for modified F1-F12 keys', () => { + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~'); + + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); + }); + }); + + describe('Third level shift', () => { + let evKeyDown; + let evKeyPress; + + beforeEach(() => { + term.handler = () => {}; + term.showCursor = () => {}; + term.clearSelection = () => {}; + // term.compositionHelper = { + // isComposing: false, + // keydown: { + // bind: () => { + // return () => { return true; }; + // } + // } + // }; + evKeyDown = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keydown', + altKey: null, + keyCode: null + }; + evKeyPress = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keypress', + altKey: null, + charCode: null, + keyCode: null + }; + }); + + describe('On Mac OS', () => { + beforeEach(() => { + term.browser.isMac = true; + }); + + it('should not interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + assert.equal(term.keyDown(evKeyDown), true); + }); + + it('should interefere with the alt + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + key on keyPress', (done) => { + let keys = ['@', '@', '\\', '\\', '|', '|']; + + term.on('keypress', (key) => { + if (key) { + let index = keys.indexOf(key); + assert(index !== -1, 'Emitted wrong key: ' + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + + describe('On MS Windows', () => { + beforeEach(() => { + term.browser.isMSWindows = true; + }); + + it('should not interfere with the alt + ctrl key on keyDown', () => { + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + evKeyPress.keyCode = 81; + assert.equal(term.keyDown(evKeyPress), true); + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + }); + + it('should interefere with the alt + ctrl + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + ctrl + key on keyPress', (done) => { + let keys = ['@', '@', '\\', '\\', '|', '|']; + + term.on('keypress', (key) => { + if (key) { + let index = keys.indexOf(key); + assert(index !== -1, 'Emitted wrong key: ' + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + }); + + describe('unicode - surrogates', () => { + it('2 characters per cell', function (): void { + this.timeout(10000); // This is needed because istanbul patches code and slows it down + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.write(high + String.fromCharCode(i)); + let tchar = term.buffer.lines.get(0)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters at last cell', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.write(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters per cell over line end with autowrap', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.wraparoundMode = true; + term.write('a' + high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters per cell over line end without autowrap', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.wraparoundMode = false; + term.write('a' + high + String.fromCharCode(i)); + // auto wraparound mode should cut off the rest of the line + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('splitted surrogates', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.write(high); + term.write(String.fromCharCode(i)); + let tchar = term.buffer.lines.get(0)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + }); + + describe('unicode - combining characters', () => { + it('café', () => { + term.write('cafe\u0301'); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('café - end of line', () => { + term.buffer.x = term.cols - 1 - 3; + term.write('cafe\u0301'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('multiple combined é', () => { + term.wraparoundMode = true; + term.write(Array(100).join('e\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('multiple surrogate with combined', () => { + term.wraparoundMode = true; + term.write(Array(100).join('\uD800\uDC00\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + }); + + describe('unicode - fullwidth characters', () => { + it('cursor movement even', () => { + expect(term.buffer.x).eql(0); + term.write('¥'); + expect(term.buffer.x).eql(2); + }); + it('cursor movement odd', () => { + term.buffer.x = 1; + expect(term.buffer.x).eql(1); + term.write('¥'); + expect(term.buffer.x).eql(3); + }); + it('line of ¥ even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('¥')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('¥')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ with combining odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('¥\u0301')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ with combining even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('¥\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of surrogate fullwidth with combining odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('\ud843\ude6d\u0301')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of surrogate fullwidth with combining even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('\ud843\ude6d\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + }); + + describe('insert mode', () => { + it('halfwidth - all', () => { + term.write(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('abcde'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e'); + expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4'); + }); + it('fullwidth - insert', () => { + term.write(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('¥¥¥'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3'); + }); + it('fullwidth - right border', () => { + term.write(Array(41).join('¥')); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('a'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced + term.write('b'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b'); + expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth + }); + }); +}); diff --git a/src/Terminal.ts b/src/Terminal.ts index 31eaef8a..a7b4b336 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,7 +29,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData, Option, StringOption, BooleanOption, StringArrayOption, NumberOption, GeometryOption, HandlerOption } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -259,8 +259,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; - public viewport: Viewport; - private compositionHelper: CompositionHelper; + public viewport: IViewport; + private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; public cols: number; @@ -601,14 +601,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (document.activeElement !== this) { return; } - self.keyDown(ev); + self._keyDown(ev); }, true); on(this.element, 'keypress', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } - self.keyPress(ev); + self._keyPress(ev); }, true); on(this.element, 'keyup', (ev: KeyboardEvent) => { @@ -618,19 +618,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }, true); on(this.textarea, 'keydown', (ev: KeyboardEvent) => { - this.keyDown(ev); + this._keyDown(ev); }, true); on(this.textarea, 'keypress', (ev: KeyboardEvent) => { - this.keyPress(ev); + this._keyPress(ev); // Truncate the textarea's value, since it is not needed this.textarea.value = ''; }, true); - on(this.textarea, 'compositionstart', this.compositionHelper.compositionstart.bind(this.compositionHelper)); - on(this.textarea, 'compositionupdate', this.compositionHelper.compositionupdate.bind(this.compositionHelper)); - on(this.textarea, 'compositionend', this.compositionHelper.compositionend.bind(this.compositionHelper)); - this.on('refresh', this.compositionHelper.updateCompositionElements.bind(this.compositionHelper)); + on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart()); + on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e)); + on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend()); + this.on('refresh', () => this.compositionHelper.updateCompositionElements()); this.on('refresh', (data) => this.queueLinkification(data.start, data.end)); } @@ -1474,21 +1474,21 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keydown event to be handled. */ - private keyDown(ev: KeyboardEvent): boolean { + protected _keyDown(ev: KeyboardEvent): boolean { if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } this.restartCursorBlinking(); - if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { + if (!this.compositionHelper.keydown(ev)) { if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); } return false; } - const result = this.evaluateKeyEscapeSequence(ev); + const result = this._evaluateKeyEscapeSequence(ev); if (result.key === C0.DC3) { // XOFF this.writeStopped = true; @@ -1501,7 +1501,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return this.cancel(ev, true); } - if (isThirdLevelShift(ev)) { + if (isThirdLevelShift(this.browser, ev)) { return true; } @@ -1529,7 +1529,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * @param ev The keyboard event to be translated to key escape sequence. */ - private evaluateKeyEscapeSequence(ev: KeyboardEvent): {cancel: boolean, key: string, scrollDisp: number} { + protected _evaluateKeyEscapeSequence(ev: KeyboardEvent): {cancel: boolean, key: string, scrollDisp: number} { const result: {cancel: boolean, key: string, scrollDisp: number} = { // Whether to cancel event propogation (NOTE: this may not be needed since the event is // canceled at the end of keyDown @@ -1836,7 +1836,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keypress event to be handled. */ - private keyPress(ev: KeyboardEvent): boolean { + protected _keyPress(ev: KeyboardEvent): boolean { let key; if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { @@ -1856,7 +1856,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(ev) + (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev) )) { return false; } @@ -2309,10 +2309,10 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo el.removeEventListener(type, handler, capture); } -function isThirdLevelShift(ev: KeyboardEvent): boolean { +function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { const thirdLevelKey = - (Browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (Browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); if (ev.type === 'keypress') { return thirdLevelKey; diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 8c7a914b..428ef098 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -4,7 +4,7 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; -import {BufferSet} from './BufferSet'; +import { BufferSet } from './BufferSet'; describe('Viewport', () => { let terminal; diff --git a/src/test/addons/test.js b/src/test/addons/test.js deleted file mode 100644 index 42ede9db..00000000 --- a/src/test/addons/test.js +++ /dev/null @@ -1,10 +0,0 @@ -var assert = require('chai').assert; -var Terminal = require('../../xterm'); - -describe('xterm.js addons', function() { - it('should load addons with Terminal.loadAddon', function () { - Terminal.loadAddon('attach'); - // Test that addon was loaded successfully - assert.equal(typeof Terminal.prototype.attach, 'function'); - }); -}); diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js deleted file mode 100644 index 51e5d017..00000000 --- a/src/test/escape-sequences-test.js +++ /dev/null @@ -1,141 +0,0 @@ -var glob = require('glob'); -var fs = require('fs'); -var os = require('os'); -var pty = require('node-pty'); -var Terminal = require('../xterm'); -var CHAR_DATA_CHAR_INDEX = require('../Buffer').CHAR_DATA_CHAR_INDEX; - -if (os.platform() === 'win32') { - // Skip tests on Windows since pty.open isn't supported - return; -} - -var CONSOLE_LOG = console.log; - -// expect files need terminal at 80x25! -var COLS = 80; -var ROWS = 25; - -/** some helpers for pty interaction */ -// we need a pty in between to get the termios decorations -// for the basic test cases a raw pty device is enough -var primitive_pty = pty.native.open(COLS, ROWS); - -// fake sychronous pty write - read -// we just pipe the data from slave to master as a child program would do -// pty.js opens pipe fds with O_NONBLOCK -// just wait 10ms instead of setting fds to blocking mode -function ptyWriteRead(s, cb) { - fs.writeSync(primitive_pty.slave, s); - setTimeout(() => { - var b = Buffer(64000); - var bytes = fs.readSync(primitive_pty.master, b, 0, 64000); - cb(b.toString('utf8', 0, bytes)); - }); -} - -// make sure raw pty is at x=0 and has no pending data -function ptyReset(cb) { - ptyWriteRead('\r\n', cb); -} - -/* debug helpers */ -// generate colorful noisy output to compare xterm and emulator cell states -function formatError(in_, out_, expected) { - function addLineNumber(start, color) { - var counter = start || 0; - return function(s) { - counter += 1; - return '\x1b[33m' + (' ' + counter).slice(-2) + color + s; - } - } - var line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; - var s = ''; - s += '\n\x1b[34m' + JSON.stringify(in_); - s += '\n\x1b[33m ' + line80 + '\n'; - s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n'); - s += '\n\x1b[33m ' + line80 + '\n'; - s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n'); - return s; -} - -// simple debug output of terminal cells -function terminalToString(term) { - var result = ''; - var line_s = ''; - for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { - line_s = ''; - for (var cell=0; cell= 0) { - continue; - } - (function(filename) { - it(filename.split('/').slice(-1)[0], done => { - ptyReset(() => { - var in_file = fs.readFileSync(filename, 'utf8'); - ptyWriteRead(in_file, from_pty => { - // uncomment this to get log from terminal - //console.log = function(){}; - - // Perform a synchronous .write(data) - xterm.writeBuffer.push(from_pty); - xterm.innerWrite(); - - var from_emulator = terminalToString(xterm); - console.log = CONSOLE_LOG; - var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); - // Some of the tests have whitespace on the right of lines, we trim all the linex - // from xterm.js so ignore this for now at least. - var expectedRightTrimmed = expected.split('\n').map(function (l) { - return l.replace(/\s+$/, ''); - }).join('\n'); - if (from_emulator != expectedRightTrimmed) { - // uncomment to get noisy output - throw new Error(formatError(in_file, from_emulator, expected)); - // throw new Error('mismatch'); - } - done(); - }); - }); - }); - })(files[i]); - } -}); - diff --git a/src/test/test.js b/src/test/test.js deleted file mode 100644 index 2c44f1e7..00000000 --- a/src/test/test.js +++ /dev/null @@ -1,889 +0,0 @@ -var assert = require('chai').assert; -var expect = require('chai').expect; -var Terminal = require('../xterm'); -var CHAR_DATA_CHAR_INDEX = require('../Buffer').CHAR_DATA_CHAR_INDEX; -var CHAR_DATA_WIDTH_INDEX = require('../Buffer').CHAR_DATA_WIDTH_INDEX; - -describe('xterm.js', function() { - var xterm; - - beforeEach(function () { - xterm = new Terminal(); - xterm.refresh = function(){}; - xterm.viewport = { - syncScrollArea: function(){} - }; - xterm.compositionHelper = { - keydown: function(){ return true; } - }; - // Force synchronous writes - xterm.write = function(data) { - xterm.writeBuffer.push(data); - xterm.innerWrite(); - }; - xterm.element = { - classList: { - toggle: function(){}, - remove: function(){} - } - }; - }); - - describe('getOption', function() { - it('should retrieve the option correctly', function() { - // In the `options` namespace. - xterm.options.cursorBlink = true; - assert.equal(xterm.getOption('cursorBlink'), true); - - // On the Terminal instance - delete xterm.options.cursorBlink; - xterm.cursorBlink = false; - assert.equal(xterm.getOption('cursorBlink'), false); - }); - it('should throw when retrieving a non-existant option', function() { - assert.throws(xterm.getOption.bind(xterm, 'fake', true)); - }); - }); - - describe('setOption', function() { - let originalWarn; - let warnCallCount; - beforeEach(() => { - originalWarn = console.warn; - warnCallCount = 0; - console.warn = () => warnCallCount++; - }); - afterEach(() => { - console.warn = originalWarn; - }); - it('should set the option correctly', function() { - xterm.setOption('cursorBlink', true); - assert.equal(xterm.cursorBlink, true); - assert.equal(xterm.options.cursorBlink, true); - xterm.setOption('cursorBlink', false); - assert.equal(xterm.cursorBlink, false); - assert.equal(xterm.options.cursorBlink, false); - }); - it('should throw when setting a non-existant option', function() { - assert.throws(xterm.setOption.bind(xterm, 'fake', true)); - }); - it('should warn and do nothing when scrollback is less than number of rows', function() { - xterm.setOption('scrollback', xterm.rows - 1); - assert.equal(xterm.getOption('scrollback'), 1000); - assert.equal(warnCallCount, 1); - }); - }); - - describe('clear', function() { - it('should clear a buffer equal to rows', function() { - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - it('should clear a buffer larger than rows', function() { - // Fill the buffer with dummy rows - for (var i = 0; i < xterm.rows * 2; i++) { - xterm.write('test\n'); - } - - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - it('should not break the prompt when cleared twice', function() { - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - }); - - describe('scroll', function() { - describe('scrollDisp', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 2; i++) { - xterm.writeln('test'); - } - startYDisp = xterm.rows + 1; - }); - it('should scroll a single line', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - xterm.scrollDisp(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should scroll multiple lines', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-5); - assert.equal(xterm.buffer.ydisp, startYDisp - 5); - xterm.scrollDisp(5); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should not scroll beyond the bounds of the buffer', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - for (var i = 0; i < startYDisp; i++) { - xterm.scrollDisp(-1); - } - assert.equal(xterm.buffer.ydisp, 0); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, 0); - }); - }); - - describe('scrollPages', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - startYDisp = (xterm.rows * 2) + 1; - }); - it('should scroll a single page', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1)); - xterm.scrollPages(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should scroll a multiple pages', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-2); - assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1) * 2); - xterm.scrollPages(2); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - }); - - describe('scrollToTop', function() { - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - }); - it('should scroll to the top', function() { - assert.notEqual(xterm.buffer.ydisp, 0); - xterm.scrollToTop(); - assert.equal(xterm.buffer.ydisp, 0); - }); - }); - - describe('scrollToBottom', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - startYDisp = (xterm.rows * 2) + 1; - }); - it('should scroll to the bottom', function() { - xterm.scrollDisp(-1); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-1); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollToTop(); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - }); - - describe('keyDown', function () { - it('should scroll down, when a key is pressed and terminal is scrolled up', function () { - // Override evaluateKeyEscapeSequence to return cancel code - xterm.evaluateKeyEscapeSequence = function() { - return { key: 'a' }; - }; - var event = { - type: 'keydown', - keyCode: 0, - preventDefault: function(){}, - stopPropagation: function(){} - }; - - xterm.buffer.ydisp = 0; - xterm.buffer.ybase = 40; - assert.notEqual(xterm.buffer.ydisp, xterm.buffer.ybase); - xterm.keyDown(event); - - // Ensure that now the terminal is scrolled to bottom - assert.equal(xterm.buffer.ydisp, xterm.buffer.ybase); - }); - - it('should not scroll down, when a custom keydown handler prevents the event', function () { - // Add some output to the terminal - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - var startYDisp = (xterm.rows * 2) + 1; - xterm.attachCustomKeyEventHandler(function () { - return false; - }); - - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - xterm.keyDown({ keyCode: 0 }); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - }); - }); - }); - - describe('evaluateKeyEscapeSequence', function() { - it('should return the correct escape sequence for unmodified keys', function() { - // Backspace - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^? - // Tab - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t'); - // Return/enter - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR - // Escape - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b'); - // Page up, page down - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ - // End, Home - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H - // Left, up, right, down arrows - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B - // Insert - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~ - // Delete - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~ - // F1-F12 - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ - }); - it('should return \\x1b[3;5~ for ctrl+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~'); - }); - it('should return \\x1b[3;2~ for shift+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~'); - }); - it('should return \\x1b[3;3~ for alt+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~'); - }); - it('should return \\x1b[5D for ctrl+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D - }); - it('should return \\x1b[5C for ctrl+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C - }); - it('should return \\x1b[5A for ctrl+up', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A - }); - it('should return \\x1b[5B for ctrl+down', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B - }); - - describe('On non-macOS platforms', function() { - beforeEach(function() { - xterm.browser.isMac = false; - }); - // Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100 - // http://unix.stackexchange.com/a/108106 - it('should return \\x1b[5D for alt+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D - }); - it('should return \\x1b[5C for alt+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C - }); - }); - - describe('On macOS platforms', function() { - beforeEach(function() { - xterm.browser.isMac = true; - }); - it('should return \\x1bb for alt+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D - }); - it('should return \\x1bf for alt+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C - }); - }); - - it('should return \\x1b[5A for alt+up', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A - }); - it('should return \\x1b[5B for alt+down', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B - }); - it('should return the correct escape sequence for modified F1-F12 keys', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~'); - - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); - }); - }); - - describe('attachCustomKeyEventHandler', function () { - var evKeyDown = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keydown' - } - var evKeyPress = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keypress' - } - - beforeEach(function() { - xterm.handler = function() {}; - xterm.showCursor = function() {}; - xterm.clearSelection = function() {}; - xterm.compositionHelper = { - keydown: { - bind: function() { - return function () { return true; } - } - }, - keypress: { - bind: function() { - return function () { return true; } - } - } - } - }); - - it('should process the keydown/keypress event based on what the handler returns', function () { - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode === 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode !== 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - }); - - it('should alive after reset(ESC c Full Reset (RIS))', function () { - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode !== 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - xterm.reset(); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - }); - }); - - describe('Third level shift', function() { - var evKeyDown = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keydown' - }, - evKeyPress = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keypress' - }; - - beforeEach(function() { - xterm.handler = function() {}; - xterm.showCursor = function() {}; - xterm.clearSelection = function() {}; - xterm.compositionHelper = { - isComposing: false, - keydown: { - bind: function() { - return function() { return true; }; - } - } - }; - }); - - describe('On Mac OS', function() { - beforeEach(function() { - xterm.browser.isMac = true; - }); - - it('should not interfere with the alt key on keyDown', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 81 })), - true - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 192 })), - true - ); - }); - - it('should interefere with the alt + arrow keys', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 37 })), - false - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 39 })), - false - ); - }); - - it('should emit key with alt + key on keyPress', function(done) { - var keys = ['@', '@', '\\', '\\', '|', '|']; - - xterm.on('keypress', function(key) { - if (key) { - var index = keys.indexOf(key); - assert(index !== -1, "Emitted wrong key: " + key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 64 })); // @ - // Firefox - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 64, keyCode: 0 })); - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 92 })); // \ - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 92, keyCode: 0 })); - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 124 })); // | - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 124, keyCode: 0 })); - }); - }); - - describe('On MS Windows', function() { - beforeEach(function() { - xterm.browser.isMSWindows = true; - }); - - it('should not interfere with the alt + ctrl key on keyDown', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 81 })), - true - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 192 })), - true - ); - }); - - it('should interefere with the alt + ctrl + arrow keys', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 37 })), - false - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 39 })), - false - ); - }); - - it('should emit key with alt + ctrl + key on keyPress', function(done) { - var keys = ['@', '@', '\\', '\\', '|', '|']; - - xterm.on('keypress', function(key) { - if (key) { - var index = keys.indexOf(key); - assert(index !== -1, "Emitted wrong key: " + key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 64 }) - ); // @ - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 64, keyCode: 0 }) - ); - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 92 }) - ); // \ - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 92, keyCode: 0 }) - ); - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 124 }) - ); // | - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 124, keyCode: 0 }) - ); - }); - }); - }); - - describe('unicode - surrogates', function() { - it('2 characters per cell', function () { - this.timeout(10000); // This is needed because istanbul patches code and slows it down - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.write(high + String.fromCharCode(i)); - var tchar = xterm.buffer.lines.get(0)[0]; - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters at last cell', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.write(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters per cell over line end with autowrap', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.wraparoundMode = true; - xterm.write('a' + high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters per cell over line end without autowrap', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.wraparoundMode = false; - xterm.write('a' + high + String.fromCharCode(i)); - // auto wraparound mode should cut off the rest of the line - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(xterm.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('splitted surrogates', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.write(high); - xterm.write(String.fromCharCode(i)); - var tchar = xterm.buffer.lines.get(0)[0]; - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - }); - - describe('unicode - combining characters', function() { - it('café', function () { - xterm.write('cafe\u0301'); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1); - }); - it('café - end of line', function() { - xterm.buffer.x = xterm.cols - 1 - 3; - xterm.write('cafe\u0301'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1); - }); - it('multiple combined é', function() { - xterm.wraparoundMode = true; - xterm.write(Array(100).join('e\u0301')); - for (var i=0; iBrowser; writeBuffer: string[]; children: HTMLElement[]; cursorHidden: boolean; @@ -184,3 +185,34 @@ export class MockBuffer implements IBuffer { throw new Error('Method not implemented.'); } } + +export class MockViewport implements IViewport { + onWheel(ev: WheelEvent): void { + throw new Error('Method not implemented.'); + } + onTouchStart(ev: TouchEvent): void { + throw new Error('Method not implemented.'); + } + onTouchMove(ev: TouchEvent): void { + throw new Error('Method not implemented.'); + } + syncScrollArea(): void { } +} + +export class MockCompositionHelper implements ICompositionHelper { + compositionstart(): void { + throw new Error('Method not implemented.'); + } + compositionupdate(ev: CompositionEvent): void { + throw new Error('Method not implemented.'); + } + compositionend(): void { + throw new Error('Method not implemented.'); + } + updateCompositionElements(dontRecurse?: boolean): void { + throw new Error('Method not implemented.'); + } + keydown(ev: KeyboardEvent): boolean { + return true; + } +}