chore: import upstream snapshot with attribution
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,100 @@
|
||||
Row ID,Order ID,Order Date,Ship Date,Ship Mode,Customer ID,Segment,Country,City,State,Postal Code,Region,Product ID,Category,Sub-Category,Sales,Quantity,Discount,Profit
|
||||
1,CA-2013-152156,11/9/2013,11/12/2013,Second Class,CG-12520,Consumer,United States,Henderson,Kentucky,42420,South,FUR-BO-10001798,Furniture,Bookcases,261.96,2,0.0,41.9136
|
||||
2,CA-2013-152156,11/9/2013,11/12/2013,Second Class,CG-12520,Consumer,United States,Henderson,Kentucky,42420,South,FUR-CH-10000454,Furniture,Chairs,731.94,3,0.0,219.582
|
||||
3,CA-2013-138688,6/13/2013,6/17/2013,Second Class,DV-13045,Corporate,United States,Los Angeles,California,90036,West,OFF-LA-10000240,Office Supplies,Labels,14.62,2,0.0,6.8714
|
||||
4,US-2012-108966,10/11/2012,10/18/2012,Standard Class,SO-20335,Consumer,United States,Fort Lauderdale,Florida,33311,South,FUR-TA-10000577,Furniture,Tables,957.5775,5,0.45,-383.031
|
||||
5,US-2012-108966,10/11/2012,10/18/2012,Standard Class,SO-20335,Consumer,United States,Fort Lauderdale,Florida,33311,South,OFF-ST-10000760,Office Supplies,Storage,22.368,2,0.2,2.5164
|
||||
6,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,FUR-FU-10001487,Furniture,Furnishings,48.86,7,0.0,14.1694
|
||||
7,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-AR-10002833,Office Supplies,Art,7.28,4,0.0,1.9656
|
||||
8,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,TEC-PH-10002275,Technology,Phones,907.152,6,0.2,90.7152
|
||||
9,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-BI-10003910,Office Supplies,Binders,18.504,3,0.2,5.7825
|
||||
10,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,OFF-AP-10002892,Office Supplies,Appliances,114.9,5,0.0,34.47
|
||||
11,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,FUR-TA-10001539,Furniture,Tables,1706.184,9,0.2,85.3092
|
||||
12,CA-2011-115812,6/9/2011,6/14/2011,Standard Class,BH-11710,Consumer,United States,Los Angeles,California,90032,West,TEC-PH-10002033,Technology,Phones,911.424,4,0.2,68.3568
|
||||
13,CA-2014-114412,4/16/2014,4/21/2014,Standard Class,AA-10480,Consumer,United States,Concord,North Carolina,28027,South,OFF-PA-10002365,Office Supplies,Paper,15.552,3,0.2,5.4432
|
||||
14,CA-2013-161389,12/6/2013,12/11/2013,Standard Class,IM-15070,Consumer,United States,Seattle,Washington,98103,West,OFF-BI-10003656,Office Supplies,Binders,407.976,3,0.2,132.5922
|
||||
15,US-2012-118983,11/22/2012,11/26/2012,Standard Class,HP-14815,Home Office,United States,Fort Worth,Texas,76106,Central,OFF-AP-10002311,Office Supplies,Appliances,68.81,5,0.8,-123.858
|
||||
16,US-2012-118983,11/22/2012,11/26/2012,Standard Class,HP-14815,Home Office,United States,Fort Worth,Texas,76106,Central,OFF-BI-10000756,Office Supplies,Binders,2.544,3,0.8,-3.816
|
||||
17,CA-2011-105893,11/11/2011,11/18/2011,Standard Class,PK-19075,Consumer,United States,Madison,Wisconsin,53711,Central,OFF-ST-10004186,Office Supplies,Storage,665.88,6,0.0,13.3176
|
||||
18,CA-2011-167164,5/13/2011,5/15/2011,Second Class,AG-10270,Consumer,United States,West Jordan,Utah,84084,West,OFF-ST-10000107,Office Supplies,Storage,55.5,2,0.0,9.99
|
||||
19,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,OFF-AR-10003056,Office Supplies,Art,8.56,2,0.0,2.4824
|
||||
20,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,TEC-PH-10001949,Technology,Phones,213.48,3,0.2,16.011
|
||||
21,CA-2011-143336,8/27/2011,9/1/2011,Second Class,ZD-21925,Consumer,United States,San Francisco,California,94109,West,OFF-BI-10002215,Office Supplies,Binders,22.72,4,0.2,7.384
|
||||
22,CA-2013-137330,12/10/2013,12/14/2013,Standard Class,KB-16585,Corporate,United States,Fremont,Nebraska,68025,Central,OFF-AR-10000246,Office Supplies,Art,19.46,7,0.0,5.0596
|
||||
23,CA-2013-137330,12/10/2013,12/14/2013,Standard Class,KB-16585,Corporate,United States,Fremont,Nebraska,68025,Central,OFF-AP-10001492,Office Supplies,Appliances,60.34,7,0.0,15.6884
|
||||
24,US-2014-156909,7/17/2014,7/19/2014,Second Class,SF-20065,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-CH-10002774,Furniture,Chairs,71.372,2,0.3,-1.0196
|
||||
25,CA-2012-106320,9/25/2012,9/30/2012,Standard Class,EB-13870,Consumer,United States,Orem,Utah,84057,West,FUR-TA-10000577,Furniture,Tables,1044.63,3,0.0,240.2649
|
||||
26,CA-2013-121755,1/16/2013,1/20/2013,Second Class,EH-13945,Consumer,United States,Los Angeles,California,90049,West,OFF-BI-10001634,Office Supplies,Binders,11.648,2,0.2,4.2224
|
||||
27,CA-2013-121755,1/16/2013,1/20/2013,Second Class,EH-13945,Consumer,United States,Los Angeles,California,90049,West,TEC-AC-10003027,Technology,Accessories,90.57,3,0.0,11.7741
|
||||
28,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-BO-10004834,Furniture,Bookcases,3083.43,7,0.5,-1665.0522
|
||||
29,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-BI-10000474,Office Supplies,Binders,9.618,2,0.7,-7.0532
|
||||
30,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,FUR-FU-10004848,Furniture,Furnishings,124.2,3,0.2,15.525
|
||||
31,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-EN-10001509,Office Supplies,Envelopes,3.264,2,0.2,1.1016
|
||||
32,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-AR-10004042,Office Supplies,Art,86.304,6,0.2,9.7092
|
||||
33,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-BI-10001525,Office Supplies,Binders,6.858,6,0.7,-5.715
|
||||
34,US-2012-150630,9/17/2012,9/21/2012,Standard Class,TB-21520,Consumer,United States,Philadelphia,Pennsylvania,19140,East,OFF-AR-10001683,Office Supplies,Art,15.76,2,0.2,3.546
|
||||
35,CA-2014-107727,10/20/2014,10/24/2014,Second Class,MA-17560,Home Office,United States,Houston,Texas,77095,Central,OFF-PA-10000249,Office Supplies,Paper,29.472,3,0.2,9.9468
|
||||
36,CA-2013-117590,12/9/2013,12/11/2013,First Class,GH-14485,Corporate,United States,Richardson,Texas,75080,Central,TEC-PH-10004977,Technology,Phones,1097.544,7,0.2,123.4737
|
||||
37,CA-2013-117590,12/9/2013,12/11/2013,First Class,GH-14485,Corporate,United States,Richardson,Texas,75080,Central,FUR-FU-10003664,Furniture,Furnishings,190.92,5,0.6,-147.963
|
||||
38,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,OFF-EN-10002986,Office Supplies,Envelopes,113.328,9,0.2,35.415
|
||||
39,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,FUR-BO-10002545,Furniture,Bookcases,532.3992,3,0.32,-46.9764
|
||||
40,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,FUR-CH-10004218,Furniture,Chairs,212.058,3,0.3,-15.147
|
||||
41,CA-2012-117415,12/27/2012,12/31/2012,Standard Class,SN-20710,Home Office,United States,Houston,Texas,77041,Central,TEC-PH-10000486,Technology,Phones,371.168,4,0.2,41.7564
|
||||
42,CA-2014-120999,9/11/2014,9/16/2014,Standard Class,LC-16930,Corporate,United States,Naperville,Illinois,60540,Central,TEC-PH-10004093,Technology,Phones,147.168,4,0.2,16.5564
|
||||
43,CA-2013-101343,7/18/2013,7/23/2013,Standard Class,RA-19885,Corporate,United States,Los Angeles,California,90049,West,OFF-ST-10003479,Office Supplies,Storage,77.88,2,0.0,3.894
|
||||
44,CA-2014-139619,9/20/2014,9/24/2014,Standard Class,ES-14080,Corporate,United States,Melbourne,Florida,32935,South,OFF-ST-10003282,Office Supplies,Storage,95.616,2,0.2,9.5616
|
||||
45,CA-2013-118255,3/12/2013,3/14/2013,First Class,ON-18715,Corporate,United States,Eagan,Minnesota,55122,Central,TEC-AC-10000171,Technology,Accessories,45.98,2,0.0,19.7714
|
||||
46,CA-2013-118255,3/12/2013,3/14/2013,First Class,ON-18715,Corporate,United States,Eagan,Minnesota,55122,Central,OFF-BI-10003291,Office Supplies,Binders,17.46,2,0.0,8.2062
|
||||
47,CA-2011-146703,10/20/2011,10/25/2011,Second Class,PO-18865,Consumer,United States,Westland,Michigan,48185,Central,OFF-ST-10001713,Office Supplies,Storage,211.96,4,0.0,8.4784
|
||||
48,CA-2013-169194,6/21/2013,6/26/2013,Standard Class,LH-16900,Consumer,United States,Dover,Delaware,19901,East,TEC-AC-10002167,Technology,Accessories,45.0,3,0.0,4.95
|
||||
49,CA-2013-169194,6/21/2013,6/26/2013,Standard Class,LH-16900,Consumer,United States,Dover,Delaware,19901,East,TEC-PH-10003988,Technology,Phones,21.8,2,0.0,6.104
|
||||
50,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,OFF-BI-10004410,Office Supplies,Binders,38.22,6,0.0,17.9634
|
||||
51,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,OFF-LA-10002762,Office Supplies,Labels,75.18,6,0.0,35.3346
|
||||
52,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,FUR-FU-10001706,Furniture,Furnishings,6.16,2,0.0,2.9568
|
||||
53,CA-2012-115742,4/18/2012,4/22/2012,Standard Class,DP-13000,Consumer,United States,New Albany,Indiana,47150,Central,FUR-CH-10003061,Furniture,Chairs,89.99,1,0.0,17.0981
|
||||
54,CA-2013-105816,12/12/2013,12/18/2013,Standard Class,JM-15265,Corporate,United States,New York City,New York,10024,East,OFF-FA-10000304,Office Supplies,Fasteners,15.26,7,0.0,6.2566
|
||||
55,CA-2013-105816,12/12/2013,12/18/2013,Standard Class,JM-15265,Corporate,United States,New York City,New York,10024,East,TEC-PH-10002447,Technology,Phones,1029.95,5,0.0,298.6855
|
||||
56,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-ST-10000604,Office Supplies,Storage,208.56,6,0.0,52.14
|
||||
57,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-PA-10001569,Office Supplies,Paper,32.4,5,0.0,15.552
|
||||
58,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,FUR-CH-10003968,Furniture,Chairs,319.41,5,0.1,7.098
|
||||
59,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-PA-10000587,Office Supplies,Paper,14.56,2,0.0,6.9888
|
||||
60,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,TEC-AC-10002167,Technology,Accessories,30.0,2,0.0,3.3
|
||||
61,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-BI-10001460,Office Supplies,Binders,48.48,4,0.2,16.362
|
||||
62,CA-2013-111682,6/18/2013,6/19/2013,First Class,TB-21055,Consumer,United States,Troy,New York,12180,East,OFF-AR-10001868,Office Supplies,Art,1.68,1,0.0,0.84
|
||||
63,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,TEC-AC-10004633,Technology,Accessories,13.98,2,0.0,6.1512
|
||||
64,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,OFF-BI-10001078,Office Supplies,Binders,25.824,6,0.2,9.3612
|
||||
65,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,OFF-PA-10003892,Office Supplies,Paper,146.73,3,0.0,68.9631
|
||||
66,CA-2012-135545,11/24/2012,11/30/2012,Standard Class,KM-16720,Consumer,United States,Los Angeles,California,90004,West,FUR-FU-10000397,Furniture,Furnishings,79.76,4,0.0,22.3328
|
||||
67,US-2012-164175,4/30/2012,5/5/2012,Standard Class,PS-18970,Home Office,United States,Chicago,Illinois,60610,Central,FUR-CH-10001146,Furniture,Chairs,213.115,5,0.3,-15.2225
|
||||
68,CA-2011-106376,12/5/2011,12/10/2011,Standard Class,BS-11590,Corporate,United States,Gilbert,Arizona,85234,West,OFF-AR-10002671,Office Supplies,Art,1113.024,8,0.2,111.3024
|
||||
69,CA-2011-106376,12/5/2011,12/10/2011,Standard Class,BS-11590,Corporate,United States,Gilbert,Arizona,85234,West,TEC-PH-10002726,Technology,Phones,167.968,4,0.2,62.988
|
||||
70,CA-2013-119823,6/5/2013,6/7/2013,First Class,KD-16270,Consumer,United States,Springfield,Virginia,22153,South,OFF-PA-10000482,Office Supplies,Paper,75.88,2,0.0,35.6636
|
||||
71,CA-2013-106075,9/19/2013,9/24/2013,Standard Class,HM-14980,Consumer,United States,New York City,New York,10009,East,OFF-BI-10004654,Office Supplies,Binders,4.616,1,0.2,1.731
|
||||
72,CA-2014-114440,9/15/2014,9/18/2014,Second Class,TB-21520,Consumer,United States,Jackson,Michigan,49201,Central,OFF-PA-10004675,Office Supplies,Paper,19.05,3,0.0,8.763
|
||||
73,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,FUR-CH-10000513,Furniture,Chairs,831.936,8,0.2,-114.3912
|
||||
74,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,FUR-FU-10003708,Furniture,Furnishings,97.04,2,0.2,1.213
|
||||
75,US-2012-134026,4/26/2012,5/2/2012,Standard Class,JE-15745,Consumer,United States,Memphis,Tennessee,38109,South,OFF-ST-10004123,Office Supplies,Storage,72.784,1,0.2,-18.196
|
||||
76,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,OFF-BI-10004182,Office Supplies,Binders,1.248,3,0.8,-1.9344
|
||||
77,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,FUR-FU-10000260,Furniture,Furnishings,9.708,3,0.6,-5.8248
|
||||
78,US-2014-118038,12/10/2014,12/12/2014,First Class,KB-16600,Corporate,United States,Houston,Texas,77041,Central,OFF-ST-10000615,Office Supplies,Storage,27.24,3,0.2,2.724
|
||||
79,US-2011-147606,11/26/2011,12/1/2011,Second Class,JE-15745,Consumer,United States,Houston,Texas,77070,Central,FUR-FU-10003194,Furniture,Furnishings,19.3,5,0.6,-14.475
|
||||
80,CA-2013-127208,6/13/2013,6/16/2013,First Class,SC-20770,Corporate,United States,Decatur,Alabama,35601,South,OFF-AP-10002118,Office Supplies,Appliances,208.16,1,0.0,56.2032
|
||||
81,CA-2013-127208,6/13/2013,6/16/2013,First Class,SC-20770,Corporate,United States,Decatur,Alabama,35601,South,OFF-BI-10002309,Office Supplies,Binders,16.74,3,0.0,8.0352
|
||||
82,CA-2011-139451,10/12/2011,10/16/2011,Standard Class,DN-13690,Consumer,United States,San Francisco,California,94122,West,OFF-AR-10002053,Office Supplies,Art,14.9,5,0.0,4.172
|
||||
83,CA-2011-139451,10/12/2011,10/16/2011,Standard Class,DN-13690,Consumer,United States,San Francisco,California,94122,West,OFF-ST-10002370,Office Supplies,Storage,21.39,1,0.0,6.2031
|
||||
84,CA-2012-149734,9/3/2012,9/8/2012,Standard Class,JC-16105,Corporate,United States,Durham,North Carolina,27707,South,OFF-EN-10000927,Office Supplies,Envelopes,200.984,7,0.2,62.8075
|
||||
85,US-2014-119662,11/14/2014,11/17/2014,First Class,CS-12400,Home Office,United States,Chicago,Illinois,60623,Central,OFF-ST-10003656,Office Supplies,Storage,230.376,3,0.2,-48.9549
|
||||
86,CA-2014-140088,5/29/2014,5/31/2014,Second Class,PO-18865,Consumer,United States,Columbia,South Carolina,29203,South,FUR-CH-10000863,Furniture,Chairs,301.96,2,0.0,33.2156
|
||||
87,CA-2014-155558,10/27/2014,11/3/2014,Standard Class,PG-18895,Consumer,United States,Rochester,Minnesota,55901,Central,TEC-AC-10001998,Technology,Accessories,19.99,1,0.0,6.7966
|
||||
88,CA-2014-155558,10/27/2014,11/3/2014,Standard Class,PG-18895,Consumer,United States,Rochester,Minnesota,55901,Central,OFF-LA-10000134,Office Supplies,Labels,6.16,2,0.0,2.9568
|
||||
89,CA-2013-159695,4/6/2013,4/11/2013,Second Class,GM-14455,Home Office,United States,Houston,Texas,77095,Central,OFF-ST-10003442,Office Supplies,Storage,158.368,7,0.2,13.8572
|
||||
90,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,OFF-AR-10004930,Office Supplies,Art,20.1,3,0.0,6.633
|
||||
91,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,TEC-PH-10004093,Technology,Phones,73.584,2,0.2,8.2782
|
||||
92,CA-2013-109806,9/18/2013,9/23/2013,Standard Class,JS-15685,Corporate,United States,Los Angeles,California,90036,West,OFF-PA-10000304,Office Supplies,Paper,6.48,1,0.0,3.1104
|
||||
93,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,OFF-PA-10003177,Office Supplies,Paper,12.96,2,0.0,6.2208
|
||||
94,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,FUR-FU-10003799,Furniture,Furnishings,53.34,3,0.0,16.5354
|
||||
95,CA-2012-149587,1/31/2012,2/5/2012,Second Class,KB-16315,Consumer,United States,Minneapolis,Minnesota,55407,Central,OFF-BI-10002852,Office Supplies,Binders,32.96,2,0.0,16.1504
|
||||
96,US-2014-109484,11/7/2014,11/13/2014,Standard Class,RB-19705,Home Office,United States,Portland,Oregon,97206,West,OFF-BI-10004738,Office Supplies,Binders,5.682,1,0.7,-3.788
|
||||
97,CA-2014-161018,11/10/2014,11/12/2014,Second Class,PN-18775,Home Office,United States,New York City,New York,10009,East,FUR-FU-10000629,Furniture,Furnishings,96.53,7,0.0,40.5426
|
||||
98,CA-2014-157833,6/18/2014,6/21/2014,First Class,KD-16345,Consumer,United States,San Francisco,California,94122,West,OFF-BI-10001721,Office Supplies,Binders,51.312,3,0.2,17.9592
|
||||
99,CA-2013-149223,9/7/2013,9/12/2013,Standard Class,ER-13855,Corporate,United States,Saint Paul,Minnesota,55106,Central,OFF-AP-10000358,Office Supplies,Appliances,77.88,6,0.0,22.5852
|
||||
|
@@ -0,0 +1,33 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/all_types_small_multi.arrow",
|
||||
);
|
||||
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(buffer);
|
||||
await viewer.load(table);
|
||||
const config = {
|
||||
plugin: "datagrid",
|
||||
};
|
||||
await viewer.restore(config);
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,30 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "http://localhost:6598/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "http://localhost:6598/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"http://localhost:6598/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, { index: "Row ID" });
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,33 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, {
|
||||
index: "Row ID",
|
||||
name: "load-viewer-csv",
|
||||
});
|
||||
await viewer.load(table);
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,41 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
|
||||
let csv = await resp.text();
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const worker = await perspective.worker();
|
||||
const table = worker.table(csv, { name: "load-viewer-superstore" });
|
||||
await viewer.load(table);
|
||||
const config = {
|
||||
plugin: "datagrid",
|
||||
group_by: ["Region", "State"],
|
||||
split_by: ["Category", "Sub-Category"],
|
||||
columns: ["Sales", "Profit"],
|
||||
master: false,
|
||||
name: "Sales Report",
|
||||
table: "load-viewer-superstore",
|
||||
linked: false,
|
||||
title: "Sales Report 2",
|
||||
};
|
||||
await viewer.restore(config);
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,42 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// Two-viewer fixture: both viewers bind to a single shared table so
|
||||
// Suite E can drive each chart independently and verify cross-chart
|
||||
// isolation (a pan on viewer A must not affect viewer B's plot
|
||||
// region; a streaming `table.update` on viewer A's table — which is
|
||||
// also viewer B's table — must surface as data updates on both
|
||||
// without blanking either).
|
||||
async function load() {
|
||||
const resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/assets/superstore.csv",
|
||||
);
|
||||
const csv = await resp.text();
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(csv, {
|
||||
index: "Row ID",
|
||||
name: "load-viewer-two-csv",
|
||||
});
|
||||
|
||||
const viewers = document.querySelectorAll("perspective-viewer");
|
||||
for (const viewer of viewers) {
|
||||
await viewer.load(table);
|
||||
}
|
||||
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,22 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
const worker = await perspective.worker();
|
||||
window.__TEST_WORKER__ = worker;
|
||||
}
|
||||
|
||||
await load();
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,36 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
async function load() {
|
||||
let resp = await fetch(
|
||||
"/node_modules/@perspective-dev/test/node_modules/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
let arrow = await resp.arrayBuffer();
|
||||
const worker = await perspective.worker();
|
||||
window.__WORKER__ = worker;
|
||||
const table = await worker.table(arrow, { name: "superstore" });
|
||||
let workspace = document.getElementById("workspace");
|
||||
window.__TABLE__ = table;
|
||||
if (workspace) {
|
||||
await workspace.load(worker);
|
||||
} else {
|
||||
window.__TABLE__ = table;
|
||||
}
|
||||
}
|
||||
|
||||
await load();
|
||||
|
||||
window.__TEST_PERSPECTIVE_READY__ = true;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@perspective-dev/test",
|
||||
"version": "4.5.2",
|
||||
"description": "Test utility based on perspective",
|
||||
"private": true,
|
||||
"main": "src/js/index.ts",
|
||||
"module": "src/js/index.ts",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"src/**/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/perspective-dev/perspective/tools/test"
|
||||
},
|
||||
"scripts": {
|
||||
"prebench": "mkdirp build",
|
||||
"bench": "node bench/versions.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"superstore-arrow": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@playwright/experimental-ct-react": "catalog:",
|
||||
"zx": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { defineConfig, devices } from "@playwright/experimental-ct-react";
|
||||
import path from "path";
|
||||
import * as dotenv from "dotenv";
|
||||
import { createRequire } from "node:module";
|
||||
import url from "node:url";
|
||||
import { execSync } from "child_process";
|
||||
// @ts-ignore -- sh_perspective.mjs is plain JS without type declarations
|
||||
import { get_scope } from "../scripts/sh_perspective.mjs";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
dotenv.config({ path: "./.perspectiverc", quiet: true });
|
||||
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
// TODO Don't hardcode this. AFAICT this can only be accomplished by choosing
|
||||
// the port before calling the playwright CLI, via env var.
|
||||
const TEST_SERVER_PORT = 6598;
|
||||
|
||||
const RUN_JUPYTERLAB = !!process.env.PSP_JUPYTERLAB_TESTS;
|
||||
|
||||
// TODO use this from core
|
||||
const package_venn = (get_scope() as string[]).reduce(
|
||||
(acc: { include: string[]; exclude: string[] }, x: string) => {
|
||||
if (x.startsWith("!")) {
|
||||
acc.exclude.push(x);
|
||||
} else {
|
||||
acc.include.push(x);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ include: [] as string[], exclude: [] as string[] },
|
||||
);
|
||||
|
||||
let PACKAGE: string[] = [];
|
||||
if (package_venn.include.length === 0) {
|
||||
PACKAGE = (
|
||||
JSON.parse(execSync(`pnpm m ls --json --depth=-1`).toString()) as {
|
||||
name?: string;
|
||||
}[]
|
||||
)
|
||||
.filter((x): x is { name: string } => x.name !== undefined)
|
||||
.map((x) => x.name.replace("@perspective-dev/", ""))
|
||||
.filter((x: string) => package_venn.exclude.indexOf(`!${x}`) === -1);
|
||||
} else {
|
||||
PACKAGE = package_venn.include.filter(
|
||||
(x: string) => package_venn.exclude.indexOf(`!${x}`) === -1,
|
||||
);
|
||||
}
|
||||
|
||||
const DEVICE_OPTIONS = {
|
||||
"Desktop Chrome": {
|
||||
// Lock DPR to 1 so WebGL viewport / canvas backing sizes are
|
||||
// identical across host machines (matters for pixel snapshots).
|
||||
// This also overrides the `devices["Desktop Chrome"]` default of 2.
|
||||
deviceScaleFactor: 1,
|
||||
launchOptions: {
|
||||
args: [
|
||||
// "--disable-accelerated-2d-canvas",
|
||||
// "--disable-gpu",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--font-render-hinting=none",
|
||||
'--proxy-server="direct://"',
|
||||
"--proxy-bypass-list=*",
|
||||
"--js-flags=--expose-gc",
|
||||
"--enable-precise-memory-info",
|
||||
// Use the software WebGL backend so GPU/driver differences
|
||||
// between machines don't leak into snapshot pixels. The
|
||||
// 2× perf hit is under budget for the test suite.
|
||||
"--use-gl=swiftshader",
|
||||
"--use-angle=swiftshader",
|
||||
"--force-color-profile=srgb",
|
||||
"--disable-lcd-text",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const BROWSER_PACKAGES = [
|
||||
{
|
||||
packageName: "viewer",
|
||||
testDir: "rust/perspective-viewer/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "react",
|
||||
testDir: "packages/react/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "viewer-datagrid",
|
||||
testDir: "packages/viewer-datagrid/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "viewer-charts",
|
||||
testDir: "packages/viewer-charts/test/ts",
|
||||
},
|
||||
{
|
||||
packageName: "jupyterlab",
|
||||
testDir: "packages/jupyterlab/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "workspace",
|
||||
testDir: "packages/workspace/test/js",
|
||||
},
|
||||
{
|
||||
packageName: "docs",
|
||||
testDir: "docs/test/js",
|
||||
},
|
||||
];
|
||||
|
||||
const NODE_PACKAGES = [
|
||||
{
|
||||
packageName: "client",
|
||||
testDir: "rust/perspective-js/test/js",
|
||||
},
|
||||
];
|
||||
|
||||
const BROWSER_AND_PYTHON_PACKAGES = [
|
||||
{
|
||||
packageName: "python-perspective-jupyterlab",
|
||||
testDir: "packages/jupyterlab/test/jupyter",
|
||||
},
|
||||
];
|
||||
|
||||
type ProjectConfig = NonNullable<
|
||||
Parameters<typeof defineConfig>[0]["projects"]
|
||||
>[number];
|
||||
|
||||
let PROJECTS = (() => {
|
||||
const acc: ProjectConfig[] = [];
|
||||
if (RUN_JUPYTERLAB) {
|
||||
for (const pkg of BROWSER_AND_PYTHON_PACKAGES) {
|
||||
for (const device of Object.keys(
|
||||
DEVICE_OPTIONS,
|
||||
) as (keyof typeof DEVICE_OPTIONS)[]) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-${device
|
||||
.toLowerCase()
|
||||
.replace(" ", "-")}`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
use: {
|
||||
...devices[device],
|
||||
// baseURL: `http://localhost:${TEST_SERVER_PORT}`,
|
||||
timezoneId: "UTC",
|
||||
launchOptions: {
|
||||
args: ["--js-flags=--expose-gc"],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const pkg of NODE_PACKAGES) {
|
||||
if (PACKAGE == undefined || PACKAGE.includes(pkg.packageName)) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-node`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const pkg of BROWSER_PACKAGES) {
|
||||
if (PACKAGE == undefined || PACKAGE.includes(pkg.packageName)) {
|
||||
for (const device of Object.keys(
|
||||
DEVICE_OPTIONS,
|
||||
) as (keyof typeof DEVICE_OPTIONS)[]) {
|
||||
acc.push({
|
||||
name: `${pkg.packageName}-${device
|
||||
.toLowerCase()
|
||||
.replace(" ", "-")}`,
|
||||
testDir: path.join(__dirname, "../../", pkg.testDir),
|
||||
use: {
|
||||
...devices[device],
|
||||
...DEVICE_OPTIONS[device],
|
||||
baseURL: `http://localhost:${TEST_SERVER_PORT}`,
|
||||
timezoneId: "UTC",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
})();
|
||||
|
||||
const __require = createRequire(import.meta.url);
|
||||
|
||||
const GLOBAL_SETUP_PATH = __require.resolve(
|
||||
"@perspective-dev/jupyterlab/test/config/jupyter/globalSetup.ts",
|
||||
);
|
||||
|
||||
const GLOBAL_TEARDOWN_PATH = __require.resolve(
|
||||
"@perspective-dev/jupyterlab/test/config/jupyter/globalTeardown.ts",
|
||||
);
|
||||
|
||||
// See https://playwright.dev/docs/test-configuration.
|
||||
export default defineConfig({
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 30_000,
|
||||
},
|
||||
repeatEach: process.env.PSP_SATURATE
|
||||
? parseInt(process.env.PSP_SATURATE)
|
||||
: 0,
|
||||
forbidOnly: !!process.env.CI,
|
||||
workers: process.env.PSP_DEBUG ? 1 : "50%",
|
||||
retries: 0,
|
||||
quiet: !process.env.PSP_DEBUG,
|
||||
reporter: process.env.CI ? [["github"], ["html"]] : [["dot"]],
|
||||
projects: PROJECTS,
|
||||
outputDir: "dist/results",
|
||||
use: {
|
||||
launchOptions: {
|
||||
slowMo: 500, // 500 milliseconds delay
|
||||
},
|
||||
headless: !process.env.PSP_HEADED,
|
||||
ctPort: 3100,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
actionTimeout: 0,
|
||||
// trace: "retain-on-failure",
|
||||
// screenshot: "only-on-failure",
|
||||
// video: "retain-on-failure",
|
||||
},
|
||||
updateSnapshots: "none",
|
||||
globalSetup: RUN_JUPYTERLAB
|
||||
? GLOBAL_SETUP_PATH
|
||||
: path.join(__dirname, "src/js/global_startup.ts"),
|
||||
globalTeardown: RUN_JUPYTERLAB
|
||||
? GLOBAL_TEARDOWN_PATH
|
||||
: path.join(__dirname, "src/js/global_teardown.ts"),
|
||||
snapshotPathTemplate:
|
||||
"dist/snapshots/{projectName}/{testFilePath}/{arg}{ext}",
|
||||
webServer: {
|
||||
command: "tsx src/js/start_test_server.ts",
|
||||
port: TEST_SERVER_PORT,
|
||||
reuseExistingServer: true,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Testing Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-all-types-small-multi-arrow.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-csv.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-superstore.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-viewer-two-csv.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.panel.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.panel.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
|
||||
perspective-viewer-datagrid::part(regular-table) {
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="panel left"><perspective-viewer></perspective-viewer></div>
|
||||
<div class="panel right"><perspective-viewer></perspective-viewer></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js"></script>
|
||||
<script type="module" src="/node_modules/@perspective-dev/test/load-workspace-arrow.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@perspective-dev/workspace/dist/css/pro.css" />
|
||||
<link rel="stylesheet" href="/node_modules/@fontsource/roboto-mono/400.css" />
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
perspective-workspace {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
perspective-workspace,
|
||||
perspective-viewer {
|
||||
font-family: "Roboto Mono" !important;
|
||||
--preload-fonts: "Roboto Mono:400";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-workspace id="workspace"></perspective-workspace>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
|
||||
type Logs = { [x: string]: string[] };
|
||||
type LogFilter = RegExp | [RegExp, { expected: boolean }];
|
||||
type ExpectedLogs = {
|
||||
[x: string]: LogFilter[];
|
||||
} & {
|
||||
push: (type: string, filter: LogFilter) => void;
|
||||
};
|
||||
export const test = base.extend<{
|
||||
consoleLogs: { logs: Logs; expectedLogs: ExpectedLogs };
|
||||
}>({
|
||||
consoleLogs: [
|
||||
async ({ page }, use) => {
|
||||
const logs: Logs = {};
|
||||
|
||||
page.setDefaultTimeout(5000);
|
||||
|
||||
page.on("console", (msg) => {
|
||||
let type = msg.type();
|
||||
let text = msg.text();
|
||||
logs[type] ? logs[type].push(text) : (logs[type] = [text]);
|
||||
});
|
||||
let expectedLogsConstructor: any = {
|
||||
warning: [[/Legacy `expressions` format/, { expected: false }]],
|
||||
error: [[/RENDERED STUB/, { expected: false }]],
|
||||
};
|
||||
|
||||
// this is crazy
|
||||
expectedLogsConstructor.push = (
|
||||
type: string,
|
||||
filter: LogFilter
|
||||
) => {
|
||||
((
|
||||
expectedLogs: ExpectedLogs,
|
||||
type: string,
|
||||
filter: LogFilter
|
||||
) => {
|
||||
expectedLogs[type]
|
||||
? expectedLogs[type].push(filter)
|
||||
: (expectedLogs[type] = [filter]);
|
||||
})(expectedLogsConstructor, type, filter);
|
||||
};
|
||||
|
||||
let expectedLogs = expectedLogsConstructor as ExpectedLogs;
|
||||
|
||||
await use({ logs, expectedLogs });
|
||||
|
||||
Object.entries(expectedLogs).forEach(([type, filters]) => {
|
||||
if (typeof filters == "function") {
|
||||
return;
|
||||
}
|
||||
for (let filter of filters) {
|
||||
let filterText: string | RegExp,
|
||||
expected = true;
|
||||
if (Array.isArray(filter)) {
|
||||
filterText = filter[0];
|
||||
expected = filter[1].expected;
|
||||
} else {
|
||||
filterText = filter;
|
||||
}
|
||||
let matched_expr = logs[type]?.find((msg) =>
|
||||
msg.match(filterText)
|
||||
);
|
||||
let msgLogs = Array.from(Object.entries(logs)).map(
|
||||
([k, v]) =>
|
||||
`"${k}": [\n\t` +
|
||||
v.map((v) => `"${v}"`).join(",\n\t") +
|
||||
"\n],\n"
|
||||
);
|
||||
let message = `Filter was "${filterText}"\nLogs were: ${msgLogs}`;
|
||||
expected
|
||||
? test
|
||||
.expect(matched_expr, {
|
||||
message,
|
||||
})
|
||||
.toBeDefined()
|
||||
: test
|
||||
.expect(matched_expr, {
|
||||
message,
|
||||
})
|
||||
.toBeUndefined();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { fetchSnapshots } from "./snapshot-sync.js";
|
||||
|
||||
export default async function run() {
|
||||
if (process.env.PSP_FETCH_SNAPSHOTS) {
|
||||
await fetchSnapshots();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { writebackSnapshots } from "./snapshot-sync.js";
|
||||
|
||||
export default async function run() {
|
||||
if (process.env.PSP_UPDATE_SNAPSHOTS) {
|
||||
await writebackSnapshots();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
export * from "./utils.ts";
|
||||
export * from "./simple_viewer_tests.ts";
|
||||
export * from "./models/page.ts";
|
||||
export { test, expect } from "./fixture.ts";
|
||||
@@ -0,0 +1,170 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, expect } from "@playwright/test";
|
||||
import { PageView } from "./page";
|
||||
import type { ColumnType } from "@perspective-dev/client";
|
||||
|
||||
export class ColumnSettingsSidebar {
|
||||
view: PageView;
|
||||
container: Locator;
|
||||
attributesTab: AttributesTab;
|
||||
styleTab: StyleTab;
|
||||
closeBtn: Locator;
|
||||
tabTitle: Locator;
|
||||
nameInputWrapper: Locator;
|
||||
nameInput: Locator;
|
||||
selectedTab: Locator;
|
||||
typeIcon: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.view = view;
|
||||
const viewer = view.container;
|
||||
this.container = viewer.locator("#column_settings_sidebar");
|
||||
this.attributesTab = new AttributesTab(this.container);
|
||||
this.styleTab = new StyleTab(this.container);
|
||||
this.closeBtn = viewer.locator("#column_settings_close_button");
|
||||
// Tab DOM now uses `.settings_tab` / `.settings_tab.selected_tab`
|
||||
// (see `containers/tab_list.rs`); the previous `.tab-padding`
|
||||
// filler span was removed, so no `:not(.tab-padding)` filter is
|
||||
// needed. These class names are shared with the viewer's query
|
||||
// tabbar (`#query_tabbar_tab`), so the locators are scoped to
|
||||
// `this.container` (the sidebar root) to avoid strict-mode
|
||||
// collisions across the two tab implementations.
|
||||
this.tabTitle = this.container.locator(".settings_tab .tab-title");
|
||||
this.nameInputWrapper = view.container.locator(
|
||||
".sidebar_header_contents",
|
||||
);
|
||||
this.nameInput = view.container.locator("input.sidebar_header_title");
|
||||
this.selectedTab = this.container.locator(
|
||||
".settings_tab.selected_tab",
|
||||
);
|
||||
this.typeIcon = this.container.locator(".type-icon");
|
||||
}
|
||||
|
||||
async openTab(name: string) {
|
||||
let locator = this.container.locator("#" + name);
|
||||
await locator.click({ timeout: 1000 });
|
||||
await this.container
|
||||
.locator(`.settings_tab.selected_tab #${name}`)
|
||||
.waitFor({ timeout: 1000 });
|
||||
}
|
||||
|
||||
async getTabs(): Promise<string[]> {
|
||||
return await this.tabTitle.allInnerTexts();
|
||||
}
|
||||
|
||||
async getSelectedTab(): Promise<string> {
|
||||
return await this.selectedTab.innerText();
|
||||
}
|
||||
|
||||
async getType(): Promise<ColumnType | "expression"> {
|
||||
const classList = await this.typeIcon.evaluate((icon) =>
|
||||
Array.from(icon.classList),
|
||||
);
|
||||
for (const ty of [
|
||||
"string",
|
||||
"boolean",
|
||||
"float",
|
||||
"integer",
|
||||
"date",
|
||||
"datetime",
|
||||
]) {
|
||||
if (classList.includes(ty)) {
|
||||
return <ColumnType>ty;
|
||||
}
|
||||
}
|
||||
if (classList.includes("expression")) {
|
||||
return "expression";
|
||||
}
|
||||
throw new Error("Unable to get a matching type for the column!");
|
||||
}
|
||||
}
|
||||
|
||||
export class AttributesTab {
|
||||
container: Locator;
|
||||
expressionEditor: ExpressionEditor;
|
||||
saveBtn: Locator;
|
||||
resetBtn: Locator;
|
||||
deleteBtn: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#attributes-tab");
|
||||
this.expressionEditor = new ExpressionEditor(this.container);
|
||||
this.saveBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-save",
|
||||
);
|
||||
this.resetBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-reset",
|
||||
);
|
||||
this.deleteBtn = this.container.locator(
|
||||
"#psp-expression-editor-button-delete",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpressionEditor {
|
||||
container: Locator;
|
||||
content: Locator;
|
||||
textarea: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#editor-container");
|
||||
this.content = this.container.locator("#content");
|
||||
this.textarea = this.container.locator("textarea");
|
||||
}
|
||||
}
|
||||
|
||||
export class StyleTab {
|
||||
container: Locator;
|
||||
contents: Locator;
|
||||
symbolsEditor: SymbolsEditor;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#style-tab");
|
||||
this.contents = parent.locator(".style_contents");
|
||||
this.symbolsEditor = new SymbolsEditor(this.container);
|
||||
}
|
||||
}
|
||||
export class SymbolsEditor {
|
||||
container: Locator;
|
||||
pairsList: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.container = parent.locator("#attributes-symbols");
|
||||
this.pairsList = this.container.locator("ul");
|
||||
}
|
||||
async getSymbolPairs() {
|
||||
const names = await this.pairsList
|
||||
.locator(".column_name")
|
||||
.allInnerTexts();
|
||||
const symbols = await this.pairsList
|
||||
.locator("option[selected]")
|
||||
.allInnerTexts();
|
||||
const zipped: SymbolPair[] = [];
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
zipped.push(new SymbolPair(names[i], symbols[i]));
|
||||
}
|
||||
return zipped;
|
||||
}
|
||||
async getPairsLength() {
|
||||
return (await this.pairsList.locator("li").all()).length;
|
||||
}
|
||||
}
|
||||
export class SymbolPair {
|
||||
key: String;
|
||||
value: String;
|
||||
constructor(key: String, value: String) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
import { ColumnSettingsSidebar } from "./column_settings.js";
|
||||
import { ColumnSelector, ColumnType, SettingsPanel } from "./settings_panel.js";
|
||||
import { DataGridPlugin } from "./plugins.js";
|
||||
import type {
|
||||
HTMLPerspectiveViewerElement,
|
||||
ViewerConfigUpdate,
|
||||
} from "@perspective-dev/viewer";
|
||||
|
||||
/**
|
||||
* This class is the primary interface between Playwright tests and the items on the Perspective Viewer.
|
||||
* It contains various subobjects such as the SettingsPanel and ColumnSettingsSidebar which contain their own
|
||||
* functionality and locators.
|
||||
*/
|
||||
export class PageView {
|
||||
readonly page: Page;
|
||||
container: Locator;
|
||||
settingsPanel: SettingsPanel;
|
||||
settingsCloseButton: Locator;
|
||||
/** Opens the settings panel. */
|
||||
settingsButton: Locator;
|
||||
columnSettingsSidebar: ColumnSettingsSidebar;
|
||||
|
||||
// plugins
|
||||
dataGrid: DataGridPlugin.DataGrid;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.container = page.locator("perspective-viewer");
|
||||
this.settingsCloseButton = this.container.locator(
|
||||
"#settings_close_button",
|
||||
);
|
||||
this.settingsButton = this.container.locator("#settings_button");
|
||||
this.columnSettingsSidebar = new ColumnSettingsSidebar(this);
|
||||
this.settingsPanel = new SettingsPanel(this);
|
||||
|
||||
this.dataGrid = new DataGridPlugin.DataGrid(page);
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.container.evaluate(async (viewer) => {
|
||||
let el = viewer as unknown as HTMLPerspectiveViewerElement;
|
||||
return await el.save();
|
||||
});
|
||||
}
|
||||
|
||||
async restore(config: ViewerConfigUpdate) {
|
||||
return this.container.evaluate(async function (viewer, config) {
|
||||
let el = viewer as unknown as HTMLPerspectiveViewerElement;
|
||||
return await el.restore(config);
|
||||
}, config as any);
|
||||
}
|
||||
|
||||
async openSettingsPanel() {
|
||||
if (await this.settingsPanel.container.isVisible()) {
|
||||
return this.settingsPanel;
|
||||
}
|
||||
await this.settingsButton.click();
|
||||
await this.settingsPanel.container.waitFor({ state: "visible" });
|
||||
return this.settingsPanel;
|
||||
}
|
||||
async closeSettingsPanel() {
|
||||
await this.settingsCloseButton.click();
|
||||
await this.settingsPanel.container.waitFor({ state: "hidden" });
|
||||
}
|
||||
|
||||
async getOrCreateColumnByType(type: ColumnType) {
|
||||
let settingsPanel = this.settingsPanel;
|
||||
await this.openSettingsPanel();
|
||||
let col = settingsPanel.activeColumns.getColumnByType(type);
|
||||
if (await col.container.isHidden()) {
|
||||
let expr: string = "";
|
||||
switch (type) {
|
||||
case "string":
|
||||
expr = "'foo'";
|
||||
break;
|
||||
case "integer":
|
||||
expr = "1";
|
||||
break;
|
||||
case "float":
|
||||
expr = "1.1";
|
||||
case "date":
|
||||
expr = "date(0,0,0)";
|
||||
case "datetime":
|
||||
expr = "now()";
|
||||
case "numeric":
|
||||
expr = "1";
|
||||
case "calendar":
|
||||
expr = "now()";
|
||||
}
|
||||
await this.settingsPanel.createNewExpression("expr", expr, true);
|
||||
await settingsPanel.activeColumns.activateColumn("expr");
|
||||
col = await settingsPanel.activeColumns.getColumnByName("expr");
|
||||
}
|
||||
await expect(col.container).toBeVisible();
|
||||
return col;
|
||||
}
|
||||
|
||||
async assureColumnSettingsOpen(column: ColumnSelector) {
|
||||
let isEditing = await column.editBtn.evaluate((btn) =>
|
||||
btn.className.includes("is-editing"),
|
||||
);
|
||||
if (!isEditing) {
|
||||
await this.container.evaluate((_) =>
|
||||
console.log("COLUMN SETTINGS CLOSED"),
|
||||
);
|
||||
await column.editBtn.click({ force: true });
|
||||
}
|
||||
expect(this.container).toBeVisible({ timeout: 1000 });
|
||||
}
|
||||
async assureColumnSettingsClosed() {
|
||||
if (await this.columnSettingsSidebar.container.isVisible()) {
|
||||
await this.columnSettingsSidebar.closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveElement() {
|
||||
return await this.container.evaluate((viewer) => {
|
||||
return viewer.shadowRoot?.activeElement;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the viewer.
|
||||
* Unlike page.
|
||||
* @param eventName
|
||||
* @param options
|
||||
* @returns A future
|
||||
*/
|
||||
async getEventListener(eventName: string, options?: { timeout?: number }) {
|
||||
let key = `__${eventName}_FIRED__`;
|
||||
await this.container.evaluate(
|
||||
(target, { eventName, key }) => {
|
||||
target.addEventListener(eventName, (event) => {
|
||||
window[key] = true;
|
||||
console.log(event);
|
||||
});
|
||||
},
|
||||
{ eventName, key },
|
||||
);
|
||||
return async () => {
|
||||
return await this.page.evaluate(
|
||||
async ({ options, key }) => {
|
||||
let i = 0;
|
||||
while (!window[key]) {
|
||||
await new Promise((x) => setTimeout(x, 10));
|
||||
let timeout = options?.timeout ?? 1000;
|
||||
if (i++ > timeout / 10) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ options, key },
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as DataGridPlugin from "./plugins/datagrid";
|
||||
|
||||
export { DataGridPlugin };
|
||||
@@ -0,0 +1,78 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect } from "@playwright/test";
|
||||
|
||||
export class DataGrid {
|
||||
element: Locator;
|
||||
regularTable: RegularTable;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.element = page.locator("perspective-viewer-datagrid");
|
||||
this.regularTable = new RegularTable(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
export class RegularTable {
|
||||
element: Locator;
|
||||
table: Locator;
|
||||
columnTitleRow: Locator;
|
||||
editBtnRow: Locator;
|
||||
openColumnEditBtn: Locator;
|
||||
realTitles: Locator;
|
||||
realEditBtns: Locator;
|
||||
|
||||
constructor(parent: Locator) {
|
||||
this.element = parent.locator("regular-table");
|
||||
this.table = this.element.locator("table");
|
||||
this.columnTitleRow = this.element.locator("#psp-column-titles");
|
||||
this.editBtnRow = this.element.locator("#psp-column-edit-buttons");
|
||||
this.openColumnEditBtn = this.editBtnRow.locator(".psp-menu-open");
|
||||
this.realTitles = this.columnTitleRow.locator(
|
||||
"th:not(.psp-header-group-corner)"
|
||||
);
|
||||
this.realEditBtns = this.editBtnRow.locator(
|
||||
"th:not(.psp-header-group-corner)"
|
||||
);
|
||||
}
|
||||
|
||||
async getTitleIdx(name: string) {
|
||||
const ths = await this.realTitles.all();
|
||||
for (const [i, locator] of ths.entries()) {
|
||||
if ((await locator.innerText()) === name) {
|
||||
this.element.evaluate(
|
||||
(_, i) => console.log("getTitleIdx returned:", i),
|
||||
i
|
||||
);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
async getEditBtnByName(name: string) {
|
||||
const n = await this.getTitleIdx(name);
|
||||
expect(n).not.toBe(-1);
|
||||
return this.editBtnRow.locator("th").nth(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the name of a column and returns a locator for the first corresponding TD in the body.
|
||||
* @param name
|
||||
*/
|
||||
async getFirstCellByColumnName(name: string) {
|
||||
const n = await this.getTitleIdx(name);
|
||||
expect(n).not.toBe(-1);
|
||||
return this.table.locator("tbody tr").first().locator("td").nth(n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { Locator, Page, expect, test } from "@playwright/test";
|
||||
import { PageView } from "./page";
|
||||
|
||||
type ViewParameter = "groupby" | "splitby" | "orderby" | "where";
|
||||
|
||||
export class SettingsPanel {
|
||||
pageView: PageView;
|
||||
container: Locator;
|
||||
closeButton: Locator;
|
||||
activeColumns: ActiveColumns;
|
||||
inactiveColumns: InactiveColumns;
|
||||
addExpressionButton: Locator;
|
||||
pluginSelector: Locator;
|
||||
groupbyInput: Locator;
|
||||
splitbyInput: Locator;
|
||||
orderbyInput: Locator;
|
||||
whereInput: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.pageView = view;
|
||||
const viewer = view.container;
|
||||
this.container = viewer.locator("#settings_panel");
|
||||
this.closeButton = viewer.locator("#settings_close_button");
|
||||
this.activeColumns = new ActiveColumns(this.pageView);
|
||||
this.inactiveColumns = new InactiveColumns(this.pageView);
|
||||
this.addExpressionButton = viewer.locator("#add-expression");
|
||||
this.pluginSelector = viewer.locator("#plugin_selector_container");
|
||||
this.groupbyInput = viewer.locator("#group_by input");
|
||||
this.splitbyInput = viewer.locator("#split_by input");
|
||||
this.orderbyInput = viewer.locator("#sort input");
|
||||
this.whereInput = viewer.locator("#filter input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and saves a new expression column.
|
||||
* @param expr
|
||||
*/
|
||||
async createNewExpression(
|
||||
name: string,
|
||||
expr: string,
|
||||
successAssertion: boolean = true
|
||||
) {
|
||||
await this.activeColumns.scrollToBottom();
|
||||
await this.addExpressionButton.click();
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
let exprEditor = sidebar.attributesTab.expressionEditor;
|
||||
await exprEditor.container.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
await sidebar.nameInput.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
expect(await exprEditor.textarea.isVisible()).toBe(true);
|
||||
expect(await sidebar.nameInput.isVisible()).toBe(true);
|
||||
|
||||
await sidebar.nameInput.focus();
|
||||
await sidebar.nameInput.clear();
|
||||
await sidebar.nameInput.type(name, { delay: 100 });
|
||||
await sidebar.nameInput.press("Enter");
|
||||
await sidebar.nameInput.blur();
|
||||
|
||||
await exprEditor.textarea.focus();
|
||||
await exprEditor.textarea.clear();
|
||||
await exprEditor.textarea.type(expr, { delay: 100 });
|
||||
await exprEditor.textarea.blur();
|
||||
|
||||
let saveBtn = this.pageView.page.locator(
|
||||
"#psp-expression-editor-button-save"
|
||||
);
|
||||
if (successAssertion) {
|
||||
expect(await saveBtn.isDisabled()).toBe(false);
|
||||
await saveBtn.click();
|
||||
await this.inactiveColumns.getColumnByName(name);
|
||||
} else {
|
||||
expect(await saveBtn.isDisabled()).toBe(true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Renames an inactive expression column.
|
||||
* @param name
|
||||
* @param expr
|
||||
*/
|
||||
async renameExpression(column: ColumnSelector, name: string) {
|
||||
await this.pageView.assureColumnSettingsOpen(column);
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
await sidebar.nameInput.waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
expect(await sidebar.nameInput.isVisible()).toBe(true);
|
||||
|
||||
await sidebar.nameInput.focus();
|
||||
await sidebar.nameInput.clear();
|
||||
await sidebar.nameInput.type(name, { delay: 100 });
|
||||
|
||||
await expect(sidebar.attributesTab.saveBtn).toBeEnabled();
|
||||
await sidebar.attributesTab.saveBtn.click();
|
||||
}
|
||||
|
||||
async editExpression(column: ColumnSelector, newExpression: string) {
|
||||
await this.pageView.assureColumnSettingsOpen(column);
|
||||
let sidebar = this.pageView.columnSettingsSidebar;
|
||||
await sidebar.openTab("Attributes");
|
||||
let exprEditor = sidebar.attributesTab.expressionEditor;
|
||||
expect(await exprEditor.textarea.isVisible()).toBe(true);
|
||||
await exprEditor.textarea.focus();
|
||||
await exprEditor.textarea.clear();
|
||||
await exprEditor.textarea.type(newExpression, { delay: 100 });
|
||||
await exprEditor.textarea.blur();
|
||||
let saveBtn = this.pageView.page.locator(
|
||||
"#psp-expression-editor-button-save"
|
||||
);
|
||||
expect(await saveBtn.isDisabled()).toBe(false);
|
||||
await saveBtn.click();
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("groupby", name)
|
||||
*/
|
||||
async groupby(name: string) {
|
||||
return await this.setViewParameter("groupby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("splitby", name)
|
||||
*/
|
||||
async splitby(name: string) {
|
||||
return await this.setViewParameter("splitby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("orderby", name)
|
||||
*/
|
||||
async orderby(name: string) {
|
||||
return await this.setViewParameter("orderby", name);
|
||||
}
|
||||
/**
|
||||
* Shorthand for setViewParamter("where", name)
|
||||
*/
|
||||
async where(name: string) {
|
||||
return await this.setViewParameter("where", name);
|
||||
}
|
||||
/**
|
||||
* Sets a view parameter ("groupby", "splitby", "orderby", or "where") to the specified column name.
|
||||
* @param type
|
||||
* @param name
|
||||
*/
|
||||
async setViewParameter(type: ViewParameter, name: string) {
|
||||
let locator: Locator;
|
||||
switch (type) {
|
||||
case "groupby":
|
||||
locator = this.groupbyInput;
|
||||
break;
|
||||
case "orderby":
|
||||
locator = this.orderbyInput;
|
||||
break;
|
||||
case "splitby":
|
||||
locator = this.splitbyInput;
|
||||
break;
|
||||
case "where":
|
||||
locator = this.whereInput;
|
||||
break;
|
||||
default:
|
||||
throw "Invalid type passed!";
|
||||
}
|
||||
await locator.type(name);
|
||||
await this.pageView.page
|
||||
.locator("perspective-dropdown .selected")
|
||||
.first() // NOTE: There probably shouldn't actually be more than one.
|
||||
.waitFor();
|
||||
await locator.press("Enter");
|
||||
}
|
||||
|
||||
async removeViewParameter(type: ViewParameter, name: string) {
|
||||
let locator: Locator;
|
||||
switch (type) {
|
||||
case "groupby":
|
||||
locator = this.container.locator("#group_by .pivot-column");
|
||||
break;
|
||||
case "orderby":
|
||||
locator = this.container.locator("#sort .pivot-column");
|
||||
break;
|
||||
case "splitby":
|
||||
locator = this.container.locator("#split_by .pivot-column");
|
||||
break;
|
||||
case "where":
|
||||
locator = this.container.locator("#filter .pivot-column");
|
||||
break;
|
||||
default:
|
||||
throw "Invalid type passed!";
|
||||
}
|
||||
await locator
|
||||
.filter({ hasText: name })
|
||||
.first()
|
||||
.locator(".row_close")
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a plugin by it's display name, i.e. the innerText of the .plugin-select-item
|
||||
* @param name
|
||||
*/
|
||||
async selectPlugin(name: string) {
|
||||
await this.pluginSelector.click();
|
||||
await this.pluginSelector.locator(`[data-plugin="${name}"]`).click();
|
||||
}
|
||||
}
|
||||
|
||||
export class ColumnSelector {
|
||||
active: boolean;
|
||||
activeBtn: Locator;
|
||||
name: Locator;
|
||||
container: Locator;
|
||||
editBtn: Locator;
|
||||
aggSelector: Locator;
|
||||
|
||||
constructor(container: Locator, active: boolean) {
|
||||
this.container = container;
|
||||
this.active = active;
|
||||
this.name = container.locator("div .column_name");
|
||||
this.aggSelector = container.locator("select");
|
||||
this.editBtn = container.locator("div .expression-edit-button");
|
||||
this.activeBtn = container.locator(".is_column_active");
|
||||
}
|
||||
}
|
||||
|
||||
export type ColumnType =
|
||||
| "integer"
|
||||
| "float"
|
||||
| "string"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "numeric"
|
||||
| "calendar";
|
||||
|
||||
// TODO: Consolidate this and InactiveColumns into a super class
|
||||
export class ActiveColumns {
|
||||
view: PageView;
|
||||
page: Page;
|
||||
container: Locator;
|
||||
topPanel: Locator;
|
||||
columnSelector: Locator;
|
||||
newColumnInput: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.page = view.page;
|
||||
this.view = view;
|
||||
this.container = view.container.locator("#active-columns");
|
||||
this.topPanel = view.container.locator("#top_panel");
|
||||
this.columnSelector = view.container.locator(
|
||||
"#active-columns :not(.top-panel) .column-selector-column"
|
||||
);
|
||||
this.newColumnInput = view.container.locator(
|
||||
".column-selector-column .column-empty input"
|
||||
);
|
||||
}
|
||||
|
||||
getFirstVisibleColumn() {
|
||||
return new ColumnSelector(this.columnSelector.first(), true);
|
||||
}
|
||||
|
||||
async getColumnByName(name: string, exact: boolean = false) {
|
||||
let locators = this.columnSelector.filter({
|
||||
hasText: name,
|
||||
});
|
||||
|
||||
let locator: Locator;
|
||||
if (exact) {
|
||||
locator = (await locators.all()).filter(
|
||||
async (l) => (await l.innerText()) === name
|
||||
)[0];
|
||||
} else {
|
||||
locator = locators.first();
|
||||
}
|
||||
|
||||
return new ColumnSelector(locator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first visible column matching the passed in type.
|
||||
* @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime"
|
||||
*/
|
||||
getColumnByType(type: ColumnType) {
|
||||
let page = this.view.page;
|
||||
let has: Locator;
|
||||
switch (type) {
|
||||
case "numeric":
|
||||
has = page.locator(".float").or(page.locator(".integer"));
|
||||
break;
|
||||
case "calendar":
|
||||
has = page.locator(".date").or(page.locator(".datetime"));
|
||||
break;
|
||||
default:
|
||||
has = page.locator(`.${type}`);
|
||||
break;
|
||||
}
|
||||
return new ColumnSelector(
|
||||
this.columnSelector.filter({ has }).first(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async visibleColumns() {
|
||||
let all = await this.columnSelector.all();
|
||||
let mapped = all.map((locator) => {
|
||||
return new ColumnSelector(locator, true);
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
|
||||
async scrollToTop() {
|
||||
this.container.focus();
|
||||
await this.container.evaluate((node) => (node.scrollTop = 0));
|
||||
}
|
||||
async scrollToBottom() {
|
||||
this.container.focus();
|
||||
await this.container.evaluate(
|
||||
(node) => (node.scrollTop = node.scrollHeight)
|
||||
);
|
||||
}
|
||||
|
||||
async toggleColumn(name: string) {
|
||||
let has = this.view.page.getByText(name);
|
||||
this.columnSelector
|
||||
.filter({ has })
|
||||
.locator(".is_column_active")
|
||||
.click();
|
||||
}
|
||||
/**
|
||||
* This function will use the empty input column to activate a column.
|
||||
* During this process, it will scroll ActiveColumns to the bottom.
|
||||
* @param name
|
||||
*/
|
||||
async activateColumn(name: string) {
|
||||
await this.scrollToBottom();
|
||||
await this.newColumnInput.waitFor({ state: "visible" });
|
||||
await this.newColumnInput.type(name);
|
||||
await this.page
|
||||
.locator("perspective-dropdown .selected")
|
||||
.first() // NOTE: There probably shouldn't actually be more than one.
|
||||
.waitFor();
|
||||
await this.newColumnInput.press("Enter");
|
||||
let addedColumn = this.columnSelector.filter({ hasText: name }).first();
|
||||
return new ColumnSelector(addedColumn!, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class InactiveColumns {
|
||||
view: PageView;
|
||||
container: Locator;
|
||||
columnSelector: Locator;
|
||||
|
||||
constructor(view: PageView) {
|
||||
this.view = view;
|
||||
this.container = view.container.locator("#sub-columns");
|
||||
this.columnSelector = this.container.locator(".column-selector-column");
|
||||
}
|
||||
|
||||
async getColumnByName(name: string) {
|
||||
await this.container.waitFor({ state: "visible", timeout: 1000 });
|
||||
let locator = this.columnSelector.filter({ hasText: name });
|
||||
return new ColumnSelector(locator, true);
|
||||
}
|
||||
async visibleColumns() {
|
||||
let all = await this.columnSelector.all();
|
||||
let mapped = all.map((locator) => {
|
||||
return new ColumnSelector(locator, false);
|
||||
});
|
||||
return mapped;
|
||||
}
|
||||
/**
|
||||
* This function will click the toggle next to the inactive column,
|
||||
* making it the only active column.
|
||||
* @param name
|
||||
*/
|
||||
async toggleColumn(name: string) {
|
||||
this.columnSelector
|
||||
.filter({ has: this.container.getByText(name) })
|
||||
.locator(".is_column_active")
|
||||
.click();
|
||||
}
|
||||
/**
|
||||
* Gets the first visible column matching the passed in type.
|
||||
* @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime"
|
||||
*/
|
||||
getColumnByType(
|
||||
type:
|
||||
| "integer"
|
||||
| "float"
|
||||
| "string"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "numeric"
|
||||
| "calendar"
|
||||
) {
|
||||
let page = this.view.page;
|
||||
let has: Locator;
|
||||
switch (type) {
|
||||
case "numeric":
|
||||
has = page.locator(".float").or(page.locator(".integer"));
|
||||
break;
|
||||
case "calendar":
|
||||
has = page.locator(".date").or(page.locator(".datetime"));
|
||||
break;
|
||||
default:
|
||||
has = page.locator(`.${type}`);
|
||||
break;
|
||||
}
|
||||
return new ColumnSelector(
|
||||
this.columnSelector.filter({ has }).first(),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const JSDOMEnvironment = require("jest-environment-jsdom");
|
||||
|
||||
module.exports = class TimezoneAwareJSDOMEnvironment extends JSDOMEnvironment {
|
||||
constructor(config, context) {
|
||||
if (context.testPath.endsWith("timezone.spec.js")) {
|
||||
process.env.TZ = "America/New_York";
|
||||
} else {
|
||||
process.env.TZ = "UTC";
|
||||
}
|
||||
|
||||
super(config);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import {
|
||||
compareContentsToSnapshot,
|
||||
getSvgContentString,
|
||||
shadow_click,
|
||||
} from "./utils.ts";
|
||||
import { test, expect } from "./index.js";
|
||||
import type { ViewerConfigUpdate } from "@perspective-dev/viewer";
|
||||
import { ViewConfigUpdate } from "@perspective-dev/client";
|
||||
|
||||
export type ContentExtractor = (page: any) => Promise<string>;
|
||||
|
||||
async function restoreViewer(page, viewerConfig: ViewerConfigUpdate) {
|
||||
return await page.evaluate(async (viewerConfig) => {
|
||||
const viewer = document.querySelector("perspective-viewer")!;
|
||||
await viewer.restore(viewerConfig);
|
||||
}, viewerConfig);
|
||||
}
|
||||
|
||||
function runSimpleCompareTest(
|
||||
viewerConfig: ViewerConfigUpdate,
|
||||
extractContent: ContentExtractor,
|
||||
snapshotPath: string[],
|
||||
) {
|
||||
return async ({ page }) => {
|
||||
await restoreViewer(page, viewerConfig);
|
||||
const content = await extractContent(page);
|
||||
await compareContentsToSnapshot(content, snapshotPath);
|
||||
};
|
||||
}
|
||||
|
||||
export function runPerspectiveEventClickTest() {
|
||||
return async ({ page }) => {
|
||||
const viewerConfig: ViewConfigUpdate = {
|
||||
filter: [["date", "<", "2025-01-01"]],
|
||||
};
|
||||
|
||||
await restoreViewer(page, viewerConfig);
|
||||
const viewer = await page.$("perspective-viewer")!;
|
||||
const perspectiveClick = viewer.evaluate(
|
||||
(element) =>
|
||||
new Promise((resolve) =>
|
||||
element.addEventListener("perspective-click", (event) => {
|
||||
// extract the detail
|
||||
resolve(event.detail);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await shadow_click(
|
||||
page,
|
||||
"perspective-viewer-datagrid",
|
||||
"regular-table > table > tbody > tr:nth-child(1) > td:nth-child(2)",
|
||||
);
|
||||
|
||||
const detail = await perspectiveClick;
|
||||
const expectedFilter = [["date", "<", "2025-01-01"]];
|
||||
const resultFilter = detail["config"]["filter"];
|
||||
expect(resultFilter).toEqual(expectedFilter);
|
||||
};
|
||||
}
|
||||
|
||||
export function run_standard_tests(
|
||||
context: string,
|
||||
extractContent: ContentExtractor,
|
||||
) {
|
||||
test("Show grid no settings", async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const viewer = document.querySelector("perspective-viewer")!;
|
||||
await viewer.getTable(); // Not sure why this is needed...
|
||||
await viewer.restore({ settings: true });
|
||||
});
|
||||
|
||||
const content = await getSvgContentString("perspective-viewer")(page);
|
||||
await compareContentsToSnapshot(content, [
|
||||
context,
|
||||
`show-grid-no-settings.txt`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("Displays visible columns", async ({ page }) => {
|
||||
await restoreViewer(page, {
|
||||
columns: ["Discount", "Profit", "Sales", "Quantity"],
|
||||
});
|
||||
|
||||
const visibleColumnContent = await extractContent(page);
|
||||
await compareContentsToSnapshot(visibleColumnContent, [
|
||||
context,
|
||||
`displays-visible-columns.txt`,
|
||||
]);
|
||||
});
|
||||
|
||||
test.describe("Pivot tests", () => {
|
||||
test(
|
||||
`Pivot by a row`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["State"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-row.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by two rows`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["Category", "Sub-Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-two-rows.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by a column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
split_by: ["Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by a row and a column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["State"],
|
||||
split_by: ["Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-row-and-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Pivot by two rows and two columns`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
group_by: ["Region", "State"],
|
||||
split_by: ["Category", "Sub-Category"],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `pivot-by-two-rows-and-two-columns.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Sort tests", () => {
|
||||
test(
|
||||
`Sort by a hidden column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "Quantity"],
|
||||
sort: [["Sales", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-hidden-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Sort by a numeric column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "Sales"],
|
||||
sort: [["Quantity", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-numeric-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Sort by an alpha column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
sort: [["State", "asc"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `sort-by-alpha-column.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Filter tests", () => {
|
||||
test(
|
||||
`Filter by a numeric column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["Sales", ">", 500]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-by-numeric-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Filter by an alpha column`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["State", "==", "Texas"]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-by-alpha-column.txt`],
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
`Filter with 'in' comparator`,
|
||||
runSimpleCompareTest(
|
||||
{
|
||||
columns: ["Row ID", "State", "Sales"],
|
||||
filter: [["State", "in", ["Texas", "California"]]],
|
||||
settings: true,
|
||||
},
|
||||
extractContent,
|
||||
[context, `filter-with-in-comparator.txt`],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const TEST_ROOT = path.resolve(__dirname, "..", "..");
|
||||
const CACHE_DIR = path.join(TEST_ROOT, "dist", "git_snapshots");
|
||||
const DEST_DIR = path.join(TEST_ROOT, "dist", "snapshots");
|
||||
const DEFAULT_REF = "master";
|
||||
|
||||
function git(args: string[], cwd: string) {
|
||||
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
||||
}
|
||||
|
||||
function remoteHasRef(
|
||||
remoteUrl: string,
|
||||
ref: string,
|
||||
// token: string | undefined,
|
||||
): boolean {
|
||||
try {
|
||||
const args: string[] = [
|
||||
"-c",
|
||||
"http.https://github.com/.extraheader=",
|
||||
"ls-remote",
|
||||
"--heads",
|
||||
remoteUrl,
|
||||
ref,
|
||||
];
|
||||
const out = execFileSync("git", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
return out.length > 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRemoteUrl(repo: string, token: string | undefined): string {
|
||||
if (token) {
|
||||
return `https://${token}@github.com/${repo}.git`;
|
||||
}
|
||||
|
||||
return `git@github.com:${repo}.git`;
|
||||
}
|
||||
|
||||
function mirrorSnapshots(srcRoot: string, dest: string) {
|
||||
const srcSnapshots = path.join(srcRoot);
|
||||
if (!fs.existsSync(srcSnapshots)) {
|
||||
throw new Error(
|
||||
`Snapshot clone at ${srcRoot} does not contain dist/snapshots/`,
|
||||
);
|
||||
}
|
||||
|
||||
fs.rmSync(dest, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(srcSnapshots, dest, { recursive: true });
|
||||
fs.rmSync(path.join(dest, ".git"), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export async function fetchSnapshots(): Promise<void> {
|
||||
const repo = process.env.PSP_SNAPSHOT_REPO;
|
||||
if (!repo) {
|
||||
throw new Error(
|
||||
"PSP_SNAPSHOT_REPO is required when fetching snapshots (e.g. 'perspective-dev/perspective-snapshots').",
|
||||
);
|
||||
}
|
||||
|
||||
const token =
|
||||
process.env.PSP_SNAPSHOT_TOKEN || process.env.GITHUB_TOKEN || undefined;
|
||||
const requestedRef = process.env.PSP_SNAPSHOT_REF || DEFAULT_REF;
|
||||
const remoteUrl = buildRemoteUrl(repo, token);
|
||||
|
||||
let ref = requestedRef;
|
||||
if (
|
||||
requestedRef !== DEFAULT_REF &&
|
||||
!remoteHasRef(remoteUrl, requestedRef)
|
||||
) {
|
||||
console.log(
|
||||
`Snapshot branch '${requestedRef}' not found on ${repo}; falling back to '${DEFAULT_REF}'.`,
|
||||
);
|
||||
|
||||
ref = DEFAULT_REF;
|
||||
}
|
||||
|
||||
const cacheGitDir = path.join(CACHE_DIR, ".git");
|
||||
if (fs.existsSync(cacheGitDir)) {
|
||||
try {
|
||||
git(["remote", "set-url", "origin", remoteUrl], CACHE_DIR);
|
||||
git(["fetch", "--depth", "1", "origin", ref], CACHE_DIR);
|
||||
git(["checkout", "-B", ref, "FETCH_HEAD"], CACHE_DIR);
|
||||
git(["reset", "--hard", "FETCH_HEAD"], CACHE_DIR);
|
||||
git(["clean", "-fdx"], CACHE_DIR);
|
||||
} catch {
|
||||
fs.rmSync(CACHE_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cacheGitDir)) {
|
||||
fs.mkdirSync(path.dirname(CACHE_DIR), { recursive: true });
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"-c",
|
||||
"http.https://github.com/.extraheader=",
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--filter=blob:none",
|
||||
"--branch",
|
||||
ref,
|
||||
remoteUrl,
|
||||
CACHE_DIR,
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Fetched snapshots from ${repo}@${ref}`);
|
||||
mirrorSnapshots(CACHE_DIR, DEST_DIR);
|
||||
}
|
||||
|
||||
export async function writebackSnapshots(): Promise<void> {
|
||||
if (!fs.existsSync(path.join(CACHE_DIR, ".git"))) {
|
||||
console.log(
|
||||
`No snapshot clone at ${CACHE_DIR}; skipping writeback. Run with --fetch-snapshots first to populate the cache.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DEST_DIR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.cpSync(DEST_DIR, CACHE_DIR, { recursive: true, force: true });
|
||||
console.log(`\nCopied updated snapshots into ${CACHE_DIR}`);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import http from "node:http";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
function read_promise(filePath: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, function (error, content) {
|
||||
if (error && error.code !== "ENOENT") {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(content);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".js": "text/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".arrow": "arraybuffer",
|
||||
".feather": "arraybuffer",
|
||||
".wasm": "application/wasm",
|
||||
};
|
||||
|
||||
async function cwd_static_file_handler(
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse<http.IncomingMessage>,
|
||||
assets = ["./"]
|
||||
) {
|
||||
let url =
|
||||
request.url
|
||||
?.split(/[\?\#]/)[0]
|
||||
.replace(/@[\^~]?\d+.[\d\*]*.[\d\*]*/, "") || "/";
|
||||
|
||||
if (url === "/") {
|
||||
url = "/index.html";
|
||||
}
|
||||
|
||||
let extname = path.extname(url);
|
||||
let contentType = CONTENT_TYPES[extname] || "text/html";
|
||||
try {
|
||||
for (const root of assets) {
|
||||
let filePath = root + url;
|
||||
let content = await read_promise(filePath);
|
||||
if (typeof content !== "undefined") {
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
|
||||
if (extname === ".arrow" || extname === ".feather") {
|
||||
response.end(content, "utf-8");
|
||||
} else {
|
||||
response.end(content);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`404 ${url}`);
|
||||
response.writeHead(404);
|
||||
response.end("", "utf-8");
|
||||
} catch (error) {
|
||||
console.error(`500 ${url}`);
|
||||
response.writeHead(500);
|
||||
response.end("", "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
const _server = http.createServer((x, y) =>
|
||||
cwd_static_file_handler(x, y, [
|
||||
path.join(__dirname, "..", "..", "..", ".."),
|
||||
])
|
||||
);
|
||||
|
||||
_server.listen({ port: 6598 });
|
||||
@@ -0,0 +1,398 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import type { ViewerConfigUpdate } from "@perspective-dev/viewer";
|
||||
import { test, expect, Locator, Page } from "@playwright/test";
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import * as prettier from "prettier";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const API_VERSION = JSON.parse(
|
||||
fs.readFileSync(__dirname + "/../../package.json").toString(),
|
||||
)["version"];
|
||||
|
||||
export const DEFAULT_CONFIG: ViewerConfigUpdate = {
|
||||
aggregates: {},
|
||||
columns_config: {},
|
||||
columns: [],
|
||||
expressions: {},
|
||||
filter: [],
|
||||
group_by: [],
|
||||
group_rollup_mode: "rollup",
|
||||
plugin: "",
|
||||
plugin_config: {},
|
||||
settings: false,
|
||||
sort: [],
|
||||
split_by: [],
|
||||
version: API_VERSION,
|
||||
table: "load-viewer-csv",
|
||||
title: null,
|
||||
theme: "Pro Light",
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean a `<svg>` for serialization/comparison.
|
||||
* @param selector
|
||||
* @param page
|
||||
* @returns
|
||||
*/
|
||||
export const getSvgContentString = (selector: string) => async (page: Page) => {
|
||||
const content = await page.evaluate(async (selector) => {
|
||||
let el = document.querySelector(selector) as Element;
|
||||
console.log(selector, el);
|
||||
|
||||
function removeAttrs(node: Element) {
|
||||
const svgAttrsToRemove = [
|
||||
"r",
|
||||
"d",
|
||||
"dx",
|
||||
"dy",
|
||||
"x",
|
||||
"y",
|
||||
"x1",
|
||||
"x2",
|
||||
"y1",
|
||||
"y2",
|
||||
"style",
|
||||
"d",
|
||||
"transform",
|
||||
"viewBox",
|
||||
"visibility",
|
||||
"style",
|
||||
];
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
for (const attr of svgAttrsToRemove) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function walkNode(node: Node) {
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ALL);
|
||||
|
||||
walk(walker, walker.currentNode as Element);
|
||||
}
|
||||
|
||||
function walk(walker: TreeWalker, node: Element) {
|
||||
if (!node) return;
|
||||
|
||||
if (node.shadowRoot) {
|
||||
walkNode(node.shadowRoot);
|
||||
}
|
||||
|
||||
removeAttrs(node);
|
||||
switch (node.nodeName) {
|
||||
case "style":
|
||||
node.textContent = "";
|
||||
break;
|
||||
case "svg":
|
||||
node.removeAttribute("viewBox");
|
||||
node.removeAttribute("height");
|
||||
break;
|
||||
case "g":
|
||||
case "path":
|
||||
case "line":
|
||||
case "circle":
|
||||
case "rect":
|
||||
case "text":
|
||||
if (
|
||||
["label", "segment"].some((c) =>
|
||||
node.classList.contains(c),
|
||||
)
|
||||
) {
|
||||
node.textContent = node.textContent?.slice(0, 2) as
|
||||
| string
|
||||
| null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
walk(walker, walker.nextNode() as Element);
|
||||
}
|
||||
|
||||
if (el?.shadowRoot) {
|
||||
el = el.shadowRoot as unknown as Element;
|
||||
}
|
||||
|
||||
let htmlString = "";
|
||||
for (const child of el.children) {
|
||||
if (child.tagName === "STYLE") {
|
||||
continue;
|
||||
}
|
||||
const clonedChild = child.cloneNode(true) as Element;
|
||||
|
||||
walkNode(clonedChild);
|
||||
|
||||
htmlString += clonedChild.outerHTML;
|
||||
}
|
||||
|
||||
return htmlString;
|
||||
}, selector);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const EXISTING_TITLES = new Map();
|
||||
|
||||
/**
|
||||
* Compares the content of an HTML element to a snapshot.
|
||||
* To generate new snapshots, run `pnpm run test --update-snapshots`.
|
||||
* This first runs the focused project(s) tests, which generates new
|
||||
* snapshots, and then updates the contents of results.tar.gz which
|
||||
* you can commit.
|
||||
* @param contents
|
||||
* @param snapshotPath
|
||||
*/
|
||||
export async function compareContentsToSnapshot(
|
||||
contents: string,
|
||||
): Promise<void> {
|
||||
// contents = contents
|
||||
// .replace(/(min-|max-)?(width|height): *\d+\.*\d+(px)?;? */g, "")
|
||||
// .replace(/style=""/g, "");
|
||||
|
||||
contents = contents.replace(/style=""/g, "");
|
||||
|
||||
const formatted = await prettier.format(contents, {
|
||||
parser: "html",
|
||||
});
|
||||
|
||||
const titlePath = test.info().titlePath;
|
||||
let snapshotPath = titlePath
|
||||
.slice(1)
|
||||
.map((s) =>
|
||||
s
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/gi, "-")
|
||||
.toLowerCase(),
|
||||
)
|
||||
.join("-");
|
||||
|
||||
const count = EXISTING_TITLES.get(snapshotPath) || 0;
|
||||
EXISTING_TITLES.set(snapshotPath, count + 1);
|
||||
if (count > 0) {
|
||||
snapshotPath = `${snapshotPath[0]}_${count}`;
|
||||
}
|
||||
|
||||
snapshotPath = `${snapshotPath}.txt`;
|
||||
await expect(formatted).toMatchSnapshot(snapshotPath);
|
||||
}
|
||||
|
||||
export async function compareSVGContentsToSnapshot(
|
||||
page: Page,
|
||||
selector: string,
|
||||
): Promise<void> {
|
||||
const svgContent = await getSvgContentString(selector)(page);
|
||||
await compareContentsToSnapshot(svgContent);
|
||||
}
|
||||
|
||||
export function getWorkspaceLightDOMContents(page: Page): Promise<string> {
|
||||
return page.evaluate(
|
||||
async () => document.querySelector("perspective-workspace")!.outerHTML,
|
||||
);
|
||||
}
|
||||
|
||||
export function getWorkspaceShadowDOMContents(page: Page): Promise<string> {
|
||||
return page.evaluate(async () => {
|
||||
return document
|
||||
.querySelector("perspective-workspace")!
|
||||
.shadowRoot!.querySelector("#container")!.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
export async function compareLightDOMContents(page: Page) {
|
||||
const contents = await getWorkspaceLightDOMContents(page);
|
||||
await compareContentsToSnapshot(contents);
|
||||
}
|
||||
|
||||
export async function compareShadowDOMContents(page: Page) {
|
||||
const contents = await getWorkspaceShadowDOMContents(page);
|
||||
await compareContentsToSnapshot(contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on an element, passing through shadow roots if necessary.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
* @param path
|
||||
*/
|
||||
export async function shadow_click(
|
||||
page: Page,
|
||||
...path: string[]
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ path }) => {
|
||||
let elem: ShadowRoot | Element | Document | null | undefined =
|
||||
document;
|
||||
while (path.length > 0) {
|
||||
if (
|
||||
elem instanceof Element &&
|
||||
(elem as Element).shadowRoot !== null
|
||||
) {
|
||||
elem = (elem as Element).shadowRoot;
|
||||
}
|
||||
|
||||
elem = elem?.querySelector(path.shift()!);
|
||||
}
|
||||
|
||||
function triggerMouseEvent(node: EventTarget, eventType: string) {
|
||||
var clickEvent = document.createEvent("MouseEvent");
|
||||
clickEvent.initEvent(eventType, true, true);
|
||||
node.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
triggerMouseEvent(elem as EventTarget, "mouseover");
|
||||
triggerMouseEvent(elem as EventTarget, "mousedown");
|
||||
triggerMouseEvent(elem as EventTarget, "mouseup");
|
||||
triggerMouseEvent(elem as EventTarget, "click");
|
||||
},
|
||||
{ path },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Types in an element, passing through shadow roots if necessary.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
* @param content
|
||||
* @param is_incremental
|
||||
* @param path
|
||||
*/
|
||||
export async function shadow_type(
|
||||
page: Page,
|
||||
content: string,
|
||||
is_incremental: boolean | string,
|
||||
...path: string[]
|
||||
): Promise<void> {
|
||||
if (typeof is_incremental !== "boolean") {
|
||||
path.unshift(is_incremental);
|
||||
is_incremental = false;
|
||||
}
|
||||
|
||||
await page.evaluate(
|
||||
async ({ content, path, is_incremental }) => {
|
||||
let elem: ShadowRoot | Element | Document | null | undefined =
|
||||
document;
|
||||
while (path.length > 0) {
|
||||
if (
|
||||
elem instanceof Element &&
|
||||
(elem as Element).shadowRoot !== null
|
||||
) {
|
||||
elem = (elem as Element).shadowRoot;
|
||||
}
|
||||
|
||||
elem = elem?.querySelector(path.shift()!);
|
||||
}
|
||||
|
||||
if (elem instanceof HTMLElement) {
|
||||
elem.focus();
|
||||
}
|
||||
|
||||
function triggerInputEvent(
|
||||
element: EventTarget | null | undefined,
|
||||
) {
|
||||
if (!element) return;
|
||||
const event = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (is_incremental) {
|
||||
while (content.length > 0) {
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
elem.value += content[0];
|
||||
} else if (
|
||||
elem instanceof HTMLElement &&
|
||||
elem.hasAttribute("contenteditable")
|
||||
) {
|
||||
elem.textContent = content;
|
||||
}
|
||||
|
||||
triggerInputEvent(elem);
|
||||
await new Promise(requestAnimationFrame);
|
||||
content = content.slice(1);
|
||||
}
|
||||
} else {
|
||||
if (elem instanceof HTMLInputElement) {
|
||||
elem.value = content;
|
||||
} else if (
|
||||
elem instanceof HTMLElement &&
|
||||
elem.hasAttribute("contenteditable")
|
||||
) {
|
||||
elem.textContent = content;
|
||||
}
|
||||
|
||||
triggerInputEvent(elem);
|
||||
}
|
||||
|
||||
if (is_incremental) {
|
||||
if (elem instanceof HTMLElement) {
|
||||
const event = new Event("change", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
elem.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
document.activeElement.blur();
|
||||
},
|
||||
{ content, path, is_incremental },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurs the active element on the page, passing through shadow roots.
|
||||
* TODO: Playwright already does this with locators.
|
||||
* @param page
|
||||
*/
|
||||
export async function shadow_blur(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
let elem = document.activeElement;
|
||||
while (elem) {
|
||||
(elem as HTMLElement).blur();
|
||||
let shadowRoot = elem.shadowRoot;
|
||||
elem = shadowRoot ? shadowRoot.activeElement : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two locators to see if they match the same node in the DOM.
|
||||
* @param left
|
||||
* @param right
|
||||
* @param page
|
||||
*/
|
||||
export async function compareNodes(left: Locator, right: Locator, page: Page) {
|
||||
let leftEl = await left.elementHandle();
|
||||
let rightEl = await right.elementHandle();
|
||||
return await page.evaluate(
|
||||
async (compare) => {
|
||||
return compare.leftEl?.isEqualNode(compare.rightEl) || false;
|
||||
},
|
||||
{
|
||||
leftEl,
|
||||
rightEl,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"declaration": true,
|
||||
"diagnostics": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist/esm",
|
||||
"rootDir": ".",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user