chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
Binary file not shown.
+100
View File
@@ -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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
46 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
47 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
48 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
49 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
50 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
51 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
52 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
53 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
54 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
55 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
56 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
57 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
58 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
59 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
60 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
61 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
62 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
63 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
64 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
65 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
66 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
67 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
68 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
69 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
70 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
71 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
72 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
73 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
74 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
75 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
76 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
77 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
78 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
79 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
80 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
81 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
82 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
83 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
84 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
85 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
86 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
87 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
88 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
89 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
90 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
91 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
92 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
93 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
94 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
95 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
96 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
97 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
98 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
99 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
100 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;
+33
View File
@@ -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;
+41
View File
@@ -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;
+42
View File
@@ -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;
+22
View File
@@ -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;
+36
View File
@@ -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;
+32
View File
@@ -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:"
}
}
+256
View File
@@ -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",
},
});
+12
View File
@@ -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>
+11
View File
@@ -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>
+31
View File
@@ -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>
+32
View File
@@ -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>
+56
View File
@@ -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>
+48
View File
@@ -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>
+102
View File
@@ -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 },
],
});
+19
View File
@@ -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();
}
}
+19
View File
@@ -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();
}
}
+16
View File
@@ -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";
+170
View File
@@ -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;
}
}
+168
View File
@@ -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 },
);
};
}
}
+15
View File
@@ -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);
}
}
+422
View File
@@ -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
);
}
}
+25
View File
@@ -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);
}
};
+249
View File
@@ -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`],
),
);
});
}
+155
View File
@@ -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}`);
}
+94
View File
@@ -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 });
+398
View File
@@ -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,
},
);
}
+17
View File
@@ -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"
}
}