1- import { nextTick } from "vue" ;
2- import { createLocalVue , createWrapper } from "@vue/test-utils" ;
3- import VueRouter from "vue-router" ;
41import { render } from "@testing-library/vue" ;
5- import Stats from "../src/components/CrashStats.vue" ;
6- import { crashStats , listBuckets } from "../src/api.js" ;
7- import { emptyCrashStats , crashStatsData , buckets } from "./fixtures.js" ;
2+ import { mount } from "@vue/test-utils" ;
83import "lodash/throttle" ;
4+ import { nextTick } from "vue" ;
5+ import { crashStats , listBuckets } from "../src/api.js" ;
6+ import Stats from "../src/components/CrashStats.vue" ;
7+ import { buckets , crashStatsData , emptyCrashStats } from "./fixtures.js" ;
98
109// This line will mock all calls to functions in ../src/api.js
1110jest . mock ( "../src/api.js" ) ;
11+
1212// Mocking calls to lodash._throttle during tests
1313jest . mock ( "lodash/throttle" , ( ) => jest . fn ( ( fn ) => fn ) ) ;
1414
1515afterEach ( jest . resetAllMocks ) ;
1616
1717test ( "empty stats doesn't break" , async ( ) => {
18- const localVue = createLocalVue ( ) ;
19- localVue . use ( VueRouter ) ;
20- const router = new VueRouter ( ) ;
21-
2218crashStats . mockResolvedValue ( emptyCrashStats ) ;
23- await render ( Stats , {
24- localVue,
25- router,
19+
20+ const { container} = render ( Stats , {
2621props :{
2722restricted :false ,
2823providers :[ ] ,
2924activityRange :14 ,
3025} ,
3126} ) ;
27+
3228await nextTick ( ) ;
3329
3430expect ( crashStats ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -37,95 +33,127 @@ test("empty stats doesn't break", async () => {
3733} ) ;
3834expect ( listBuckets ) . toHaveBeenCalledTimes ( 0 ) ;
3935
40- await nextTick ( ) ;
4136// Assert no signature is displayed in the table
42- expect ( document . querySelector ( "tbody tr" ) ) . toBeNull ( ) ;
37+ expect ( container . querySelector ( "tbody tr" ) ) . toBeNull ( ) ;
4338} ) ;
4439
4540test ( "stats are shown" , async ( ) => {
46- const localVue = createLocalVue ( ) ;
47- localVue . use ( VueRouter ) ;
48- const router = new VueRouter ( ) ;
49-
5041crashStats . mockResolvedValue ( crashStatsData ) ;
5142listBuckets . mockResolvedValue ( buckets ) ;
52- const { getByText} = await render ( Stats , {
53- localVue,
54- router,
43+
44+ const { getByText, container} = render ( Stats , {
5545props :{
5646restricted :false ,
5747providers :[ ] ,
5848activityRange :14 ,
5949} ,
6050} ) ;
51+
6152await nextTick ( ) ;
6253
6354expect ( crashStats ) . toHaveBeenCalledTimes ( 1 ) ;
6455expect ( crashStats ) . toHaveBeenCalledWith ( {
6556ignore_toolfilter :"0" ,
6657} ) ;
6758expect ( listBuckets ) . toHaveBeenCalledTimes ( 1 ) ;
68- await nextTick ( ) ;
69- await nextTick ( ) ;
59+
7060await nextTick ( ) ;
7161
72- // Assert two signatures are displayed in the table
73- expect ( document . querySelectorAll ( "tbody tr" ) . length ) . toBe ( 2 ) ;
62+ expect ( container . querySelectorAll ( "tbody tr" ) . length ) . toBe ( 2 ) ;
7463getByText ( "A short description for bucket 1" ) ;
7564const buttonLink = getByText ( "1630739" ) ;
76- expect ( buttonLink ) . toHaveProperty ( "href" , buckets [ 0 ] . bug_urltemplate ) ;
77- expect ( buttonLink ) . toHaveProperty ( "target" , "_blank" ) ;
65+ expect ( buttonLink . getAttribute ( "href" ) ) . toBe ( buckets [ 0 ] . bug_urltemplate ) ;
66+ expect ( buttonLink . getAttribute ( "target" ) ) . toBe ( "_blank" ) ;
7867getByText ( "A short description for bucket 2" ) ;
7968getByText ( "Assign an existing bug" ) ;
8069} ) ;
8170
8271test ( "stats use hash params" , async ( ) => {
8372crashStats . mockResolvedValue ( crashStatsData ) ;
8473listBuckets . mockResolvedValue ( buckets ) ;
85- const localVue = createLocalVue ( ) ;
86- const $route = { path :"/stats" , hash :"#sort=id&alltools=1" } ;
87- const $router = [ ] ;
88- await render ( Stats , {
89- localVue,
90- mocks :{
91- $route,
92- $router,
93- } ,
74+
75+ const { container} = render ( Stats , {
9476props :{
9577restricted :false ,
9678providers :[ ] ,
9779activityRange :14 ,
9880} ,
81+ global :{
82+ mocks :{
83+ $route :{ path :"/stats" , hash :"#sort=id&alltools=1" } ,
84+ $router :[ ] ,
85+ } ,
86+ } ,
9987} ) ;
88+
10089await nextTick ( ) ;
10190
10291expect ( crashStats ) . toHaveBeenCalledTimes ( 1 ) ;
10392expect ( crashStats ) . toHaveBeenCalledWith ( {
10493ignore_toolfilter :"1" ,
10594} ) ;
10695expect ( listBuckets ) . toHaveBeenCalledTimes ( 1 ) ;
107- await nextTick ( ) ;
108- await nextTick ( ) ;
96+
10997await nextTick ( ) ;
11098
111- expect ( document . querySelectorAll ( "tbody tr" ) . length ) . toBe ( 2 ) ;
99+ expect ( container . querySelectorAll ( "tbody tr" ) . length ) . toBe ( 2 ) ;
112100} ) ;
113101
114102test ( "stats are sortable" , async ( ) => {
115103crashStats . mockResolvedValue ( crashStatsData ) ;
116104listBuckets . mockResolvedValue ( buckets ) ;
117- const localVue = createLocalVue ( ) ;
118- localVue . use ( VueRouter ) ;
119- const router = new VueRouter ( ) ;
120- await render ( Stats , {
121- localVue,
122- router,
105+ // Create a reactive route object that can be updated
106+ const route = {
107+ hash :"" ,
108+ } ;
109+
110+ // Create a mock router with push method
111+ const router = {
112+ push :jest . fn ( ( value ) => {
113+ const newHash = value . hash . slice ( 1 ) ;
114+ const currentHash = route . hash . slice ( 1 ) ;
115+
116+ // Get the new sort parameter (e.g., "id" or "-id")
117+ const newSortParam = newHash . split ( "=" ) [ 1 ] ;
118+ const newSortKey = newSortParam . replace ( / ^ - / , "" ) ;
119+
120+ const currentParams = currentHash ?currentHash . split ( "," ) :[ ] ;
121+
122+ const otherParams = currentParams
123+ . filter ( ( param ) => {
124+ if ( ! param . startsWith ( "sort=" ) ) return true ;
125+ const existingSortKey = param . split ( "=" ) [ 1 ] . replace ( / ^ - / , "" ) ;
126+ return existingSortKey !== newSortKey ;
127+ } )
128+ . map ( ( param ) => {
129+ if ( param . startsWith ( "sort=" ) ) {
130+ return param . slice ( 5 ) ;
131+ }
132+ return param ;
133+ } ) ;
134+
135+ // Combine the new sort with existing parameters
136+ route . hash = "#" + [ newHash , ...otherParams ] . filter ( Boolean ) . join ( "," ) ;
137+ } ) ,
138+ } ;
139+
140+ const wrapper = mount ( Stats , {
123141props :{
124142restricted :false ,
125143providers :[ ] ,
126144activityRange :14 ,
127145} ,
146+ global :{
147+ stubs :{
148+ RouterLink :true ,
149+ } ,
150+ mocks :{
151+ $route :route ,
152+ $router :router ,
153+ } ,
154+ } ,
128155} ) ;
156+
129157await nextTick ( ) ;
130158
131159expect ( crashStats ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -134,38 +162,36 @@ test("stats are sortable", async () => {
134162await nextTick ( ) ;
135163await nextTick ( ) ;
136164
137- let rows = document . querySelectorAll ( "tbody tr" ) ;
165+ let rows = wrapper . findAll ( "tbody tr" ) ;
138166expect ( rows . length ) . toBe ( 2 ) ;
139167// sorted by daily count by default
140- expect ( rows [ 0 ] . querySelector ( "td" ) . textContent ) . toBe ( "2" ) ;
141- expect ( rows [ 1 ] . querySelector ( "td" ) . textContent ) . toBe ( "1" ) ;
142- expect ( router . currentRoute . hash ) . toBe ( "" ) ;
168+ expect ( rows [ 0 ] . find ( "td" ) . text ( ) ) . toBe ( "2" ) ;
169+ expect ( rows [ 1 ] . find ( "td" ) . text ( ) ) . toBe ( "1" ) ;
170+ expect ( route . hash ) . toBe ( "" ) ;
143171
144- await createWrapper ( document . querySelector ( "thead th" ) ) . trigger ( "click" ) ;
172+ await wrapper . find ( "theadtr th" ) . trigger ( "click" ) ;
145173await nextTick ( ) ;
146174
147- rows = document . querySelectorAll ( "tbody tr" ) ;
175+ rows = wrapper . findAll ( "tbody tr" ) ;
148176expect ( rows . length ) . toBe ( 2 ) ;
149177// sorted by id now
150- expect ( rows [ 0 ] . querySelector ( "td" ) . textContent ) . toBe ( "2" ) ;
151- expect ( rows [ 1 ] . querySelector ( "td" ) . textContent ) . toBe ( "1" ) ;
152- expect ( router . currentRoute . hash ) . toBe ( "#sort=-id" ) ;
178+ expect ( rows [ 0 ] . find ( "td" ) . text ( ) ) . toBe ( "2" ) ;
179+ expect ( rows [ 1 ] . find ( "td" ) . text ( ) ) . toBe ( "1" ) ;
180+ expect ( router . push ) . toHaveBeenCalledTimes ( 1 ) ;
181+ expect ( route . hash ) . toBe ( "#sort=-id" ) ;
153182
154- await createWrapper ( document . querySelector ( "thead th" ) ) . trigger ( "click" ) ;
183+ await wrapper . find ( "thead th" ) . trigger ( "click" ) ;
155184await nextTick ( ) ;
156185
157- rows = document . querySelectorAll ( "tbody tr" ) ;
186+ rows = wrapper . findAll ( "tbody tr" ) ;
158187expect ( rows . length ) . toBe ( 2 ) ;
159188// sorted by -id now
160- expect ( rows [ 0 ] . querySelector ( "td" ) . textContent ) . toBe ( "1" ) ;
161- expect ( rows [ 1 ] . querySelector ( "td" ) . textContent ) . toBe ( "2" ) ;
162- expect ( router . currentRoute . hash ) . toBe ( "#sort=id" ) ;
163-
164- await createWrapper ( document . querySelector ( "thead th + th" ) ) . trigger (
165- "click" ,
166- { ctrlKey :true } ,
167- ) ;
189+ expect ( rows [ 0 ] . find ( "td" ) . text ( ) ) . toBe ( "1" ) ;
190+ expect ( rows [ 1 ] . find ( "td" ) . text ( ) ) . toBe ( "2" ) ;
191+ expect ( route . hash ) . toBe ( "#sort=id" ) ;
192+
193+ await wrapper . find ( "thead th + th" ) . trigger ( "click" , { key :"Control" } ) ;
168194await nextTick ( ) ;
169195
170- expect ( router . currentRoute . hash ) . toBe ( "#sort=-shortDescription,id" ) ;
196+ expect ( route . hash ) . toBe ( "#sort=-shortDescription,id" ) ;
171197} ) ;