getSortOrder.test.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import resolveConfig from '../src/public/resolve-config'
  2. import { createContext } from '../src/lib/setupContextUtils'
  3. import bigSign from '../src/util/bigSign'
  4. /**
  5. * This is a function that the prettier-plugin-tailwindcss would use. It would
  6. * do the actual sorting based on the classes and order we return from `getClassOrder`.
  7. *
  8. * This way the actual sorting logic is done in the plugin which allows you to
  9. * put unknown classes at the end for example.
  10. *
  11. * @param {Array<[string, bigint]>} arrayOfTuples
  12. * @returns {string}
  13. */
  14. function defaultSort(arrayOfTuples) {
  15. return arrayOfTuples
  16. .sort(([, a], [, z]) => {
  17. if (a === z) return 0
  18. if (a === null) return -1
  19. if (z === null) return 1
  20. return bigSign(a - z)
  21. })
  22. .map(([className]) => className)
  23. .join(' ')
  24. }
  25. it('should return a list of tuples with the sort order', () => {
  26. let input = 'font-bold underline hover:font-medium unknown'
  27. let config = {}
  28. let context = createContext(resolveConfig(config))
  29. expect(context.getClassOrder(input.split(' '))).toEqual([
  30. ['font-bold', expect.any(BigInt)],
  31. ['underline', expect.any(BigInt)],
  32. ['hover:font-medium', expect.any(BigInt)],
  33. // Unknown values receive `null`
  34. ['unknown', null],
  35. ])
  36. })
  37. it.each([
  38. // Utitlies
  39. ['px-3 p-1 py-3', 'p-1 px-3 py-3'],
  40. // Utitlies and components
  41. ['px-4 container', 'container px-4'],
  42. // Utilities with variants
  43. ['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
  44. // Utitlies with important
  45. ['px-3 !py-4', '!py-4 px-3'],
  46. ['!py-4 px-3', '!py-4 px-3'],
  47. // Components with variants
  48. ['hover:container container', 'container hover:container'],
  49. // Components and utilities with variants
  50. [
  51. 'focus:hover:container hover:underline hover:container p-1',
  52. 'p-1 hover:container focus:hover:container hover:underline',
  53. ],
  54. // Leave user css order alone, and move to the front
  55. ['b p-1 a', 'b a p-1'],
  56. ['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
  57. // Add special treatment for `group` and `peer`
  58. ['a peer container underline', 'a peer container underline'],
  59. ])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
  60. let config = {}
  61. let context = createContext(resolveConfig(config))
  62. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  63. })
  64. it.each([
  65. // Utitlies
  66. ['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
  67. // Utitlies and components
  68. ['tw-px-4 tw-container', 'tw-container tw-px-4'],
  69. // Utilities with variants
  70. [
  71. 'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
  72. 'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
  73. ],
  74. // Utitlies with important
  75. ['tw-px-3 !tw-py-4', '!tw-py-4 tw-px-3'],
  76. ['!tw-py-4 tw-px-3', '!tw-py-4 tw-px-3'],
  77. // Components with variants
  78. ['hover:tw-container tw-container', 'tw-container hover:tw-container'],
  79. // Components and utilities with variants
  80. [
  81. 'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
  82. 'tw-p-1 hover:tw-container focus:hover:tw-container hover:tw-underline',
  83. ],
  84. // Leave user css order alone, and move to the front
  85. ['b tw-p-1 a', 'b a tw-p-1'],
  86. ['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
  87. // Add special treatment for `group` and `peer`
  88. ['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
  89. ])(
  90. 'should sort "%s" with prefixex based on the order we generate them in to "%s"',
  91. (input, output) => {
  92. let config = { prefix: 'tw-' }
  93. let context = createContext(resolveConfig(config))
  94. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  95. }
  96. )
  97. it('sorts classes deterministically across multiple class lists', () => {
  98. let classes = [
  99. [
  100. 'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500',
  101. 'a-class b-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
  102. ],
  103. [
  104. 'px-3 b-class p-1 py-3 bg-blue-500 a-class bg-red-500',
  105. 'b-class a-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
  106. ],
  107. ]
  108. let config = {}
  109. // Same context, different class lists
  110. let context = createContext(resolveConfig(config))
  111. for (const [input, output] of classes) {
  112. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  113. }
  114. // Different context, different class lists
  115. for (const [input, output] of classes) {
  116. context = createContext(resolveConfig(config))
  117. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  118. }
  119. })
  120. it('sorts based on first occurrence of a candidate / rule', () => {
  121. let classes = [
  122. ['foo-1 foo', 'foo foo-1'],
  123. ['bar', 'bar'],
  124. ['foo-1 foo', 'foo foo-1'],
  125. ]
  126. let config = {
  127. theme: {},
  128. plugins: [
  129. function ({ addComponents }) {
  130. addComponents({
  131. '.foo': { display: 'block' },
  132. '.foo-1': { display: 'block' },
  133. '.bar': { display: 'block' },
  134. // This rule matches both the candidate `foo` and `bar`
  135. // But when sorting `foo` — we've already got a
  136. // position for `foo` so we should use it
  137. '.bar .foo': { display: 'block' },
  138. })
  139. },
  140. ],
  141. }
  142. // Same context, different class lists
  143. let context = createContext(resolveConfig(config))
  144. for (const [input, output] of classes) {
  145. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  146. }
  147. // Different context, different class lists
  148. for (const [input, output] of classes) {
  149. context = createContext(resolveConfig(config))
  150. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  151. }
  152. })
  153. it('Sorting is unchanged when multiple candidates share the same rule / object', () => {
  154. let classes = [
  155. ['x y', 'x y'],
  156. ['a', 'a'],
  157. ['x y', 'x y'],
  158. ]
  159. let config = {
  160. theme: {},
  161. plugins: [
  162. function ({ addComponents }) {
  163. addComponents({
  164. '.x': { color: 'red' },
  165. '.a': { color: 'red' },
  166. // This rule matches both the candidate `a` and `y`
  167. // When sorting x and y first we would keep that sort order
  168. // Then sorting `a` we would end up replacing the candidate on the rule
  169. // Thus causing `y` to no longer have a sort order causing it to be sorted
  170. // first by accident
  171. '.y .a': { color: 'red' },
  172. })
  173. },
  174. ],
  175. }
  176. // Same context, different class lists
  177. let context = createContext(resolveConfig(config))
  178. for (const [input, output] of classes) {
  179. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  180. }
  181. // Different context, different class lists
  182. for (const [input, output] of classes) {
  183. context = createContext(resolveConfig(config))
  184. expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
  185. }
  186. })