test-box-properties.js (13064B)
1 import { 2 testElement, 3 writingModes, 4 testCSSValues, 5 testComputedValues, 6 makeDeclaration 7 } from "./test-shared.js"; 8 9 // Values to use while testing 10 const testValues = { 11 "length": ["1px", "2px", "3px", "4px", "5px"], 12 "color": ["rgb(1, 1, 1)", "rgb(2, 2, 2)", "rgb(3, 3, 3)", "rgb(4, 4, 4)", "rgb(5, 5, 5)"], 13 "border-style": ["solid", "dashed", "dotted", "double", "groove"], 14 }; 15 16 /** 17 * Creates a group of physical and logical box properties, such as 18 * 19 * { physical: { 20 * left: "margin-left", right: "margin-right", 21 * top: "margin-top", bottom: "margin-bottom", 22 * }, logical: { 23 * inlineStart: "margin-inline-start", inlineEnd: "margin-inline-end", 24 * blockStart: "margin-block-start", blockEnd: "margin-block-end", 25 * }, shorthands: { 26 * "margin": ["margin-top", "margin-right", "margin-bottom", "margin-left"], 27 * "margin-inline": ["margin-inline-start", "margin-inline-end"], 28 * "margin-block": ["margin-block-start", "margin-block-end"], 29 * }, type: ["length"], prerequisites: "...", property: "margin-*" } 30 * 31 * @param {string} property 32 * A string representing the property names, like "margin-*". 33 * @param {Object} descriptor 34 * @param {string|string[]} descriptor.type 35 * Describes the kind of values accepted by the property, like "length". 36 * Must be a key or a collection of keys from the `testValues` object. 37 * @param {Object={}} descriptor.prerequisites 38 * Represents property declarations that are needed by `property` to work. 39 * For example, border-width properties require a border style. 40 */ 41 export function createBoxPropertyGroup(property, descriptor) { 42 const logical = {}; 43 const physical = {}; 44 const shorthands = {}; 45 for (const axis of ["inline", "block"]) { 46 const shorthand = property.replace("*", axis); 47 const longhands = []; 48 shorthands[shorthand] = longhands; 49 for (const side of ["start", "end"]) { 50 const logicalSide = axis + "-" + side; 51 const camelCase = logicalSide.replace(/-(.)/g, (match, $1) => $1.toUpperCase()); 52 const longhand = property.replace("*", logicalSide); 53 logical[camelCase] = longhand; 54 longhands.push(longhand); 55 } 56 } 57 const isInset = property === "inset-*"; 58 let prerequisites = ""; 59 for (const physicalSide of ["left", "right", "top", "bottom"]) { 60 physical[physicalSide] = isInset ? physicalSide : property.replace("*", physicalSide); 61 prerequisites += makeDeclaration(descriptor.prerequisites, physicalSide); 62 } 63 shorthands[property.replace("-*", "")] = 64 ["top", "right", "bottom", "left"].map(physicalSide => physical[physicalSide]); 65 const type = [].concat(descriptor.type); 66 return {logical, physical, shorthands, type, prerequisites, property}; 67 } 68 69 /** 70 * Creates a group physical and logical box-corner properties. 71 * 72 * @param {string} property 73 * A string representing the property names, like "border-*-radius". 74 * @param {Object} descriptor 75 * @param {string|string[]} descriptor.type 76 * Describes the kind of values accepted by the property, like "length". 77 * Must be a key or a collection of keys from the `testValues` object. 78 * @param {Object={}} descriptor.prerequisites 79 * Represents property declarations that are needed by `property` to work. 80 * For example, border-width properties require a border style. 81 */ 82 export function createCornerPropertyGroup(property, descriptor) { 83 const logical = {}; 84 const physical = {}; 85 const shorthands = {}; 86 for (const logicalCorner of ["start-start", "start-end", "end-start", "end-end"]) { 87 const prop = property.replace("*", logicalCorner); 88 const [block_side, inline_side] = logicalCorner.split("-"); 89 const b = "block" + block_side.charAt(0).toUpperCase() + block_side.slice(1); 90 const i = "inline" + inline_side.charAt(0).toUpperCase() + inline_side.slice(1); 91 const index = b + "-" + i; // e.g. "blockStart-inlineEnd" 92 logical[index] = prop; 93 } 94 let prerequisites = ""; 95 for (const physicalCorner of ["top-left", "top-right", "bottom-left", "bottom-right"]) { 96 const prop = property.replace("*", physicalCorner); 97 physical[physicalCorner] = prop; 98 prerequisites += makeDeclaration(descriptor.prerequisites, physicalCorner); 99 } 100 const type = [].concat(descriptor.type); 101 return {logical, physical, shorthands, type, prerequisites, property}; 102 } 103 104 /** 105 * Creates a group of physical and logical sizing properties. 106 * 107 * @param {string} prefix 108 * One of "", "max-" or "min-". 109 */ 110 export function createSizingPropertyGroup(prefix) { 111 return { 112 logical: { 113 inline: `${prefix}inline-size`, 114 block: `${prefix}block-size`, 115 }, 116 physical: { 117 horizontal: `${prefix}width`, 118 vertical: `${prefix}height`, 119 }, 120 type: ["length"], 121 prerequisites: makeDeclaration({display: "block"}), 122 property: (prefix ? prefix.slice(0, -1) + " " : "") + "sizing", 123 }; 124 } 125 126 /** 127 * Tests a grup of logical and physical properties in different writing modes. 128 * 129 * @param {Object} group 130 * An object returned by createBoxPropertyGroup or createSizingPropertyGroup. 131 */ 132 export function runTests(group) { 133 const values = testValues[group.type[0]].map(function(_, i) { 134 return group.type.map(type => testValues[type][i]).join(" "); 135 }); 136 const logicals = Object.values(group.logical); 137 const physicals = Object.values(group.physical); 138 const shorthands = group.shorthands ? Object.entries(group.shorthands) : null; 139 const is_corner = group.property == "border-*-radius"; 140 141 test(function() { 142 const expected = []; 143 for (const [i, logicalProp] of logicals.entries()) { 144 testElement.style.setProperty(logicalProp, values[i]); 145 expected.push([logicalProp, values[i]]); 146 } 147 testCSSValues("logical properties in inline style", testElement.style, expected); 148 }, `Test that logical ${group.property} properties are supported.`); 149 testElement.style.cssText = ""; 150 151 const shorthandValues = {}; 152 for (const [shorthand, longhands] of shorthands || []) { 153 let valueArray; 154 if (group.type.length > 1) { 155 valueArray = [values[0]]; 156 } else { 157 valueArray = testValues[group.type].slice(0, longhands.length); 158 } 159 shorthandValues[shorthand] = valueArray; 160 const value = valueArray.join(" "); 161 const expected = [[shorthand, value]]; 162 for (let [i, longhand] of longhands.entries()) { 163 expected.push([longhand, valueArray[group.type.length > 1 ? 0 : i]]); 164 } 165 test(function() { 166 testElement.style.setProperty(shorthand, value); 167 testCSSValues("shorthand in inline style", testElement.style, expected); 168 const stylesheet = `.test { ${group.prerequisites} }`; 169 testComputedValues("shorthand in computed style", stylesheet, expected); 170 }, `Test that ${shorthand} shorthand sets longhands and serializes correctly.`); 171 testElement.style.cssText = ""; 172 } 173 174 for (const writingMode of writingModes) { 175 for (const style of writingMode.styles) { 176 const writingModeDecl = makeDeclaration(style); 177 178 const associated = {}; 179 for (const [logicalSide, logicalProp] of Object.entries(group.logical)) { 180 let physicalProp; 181 if (is_corner) { 182 const [ block_side, inline_side] = logicalSide.split("-"); 183 const physicalSide1 = writingMode[block_side]; 184 const physicalSide2 = writingMode[inline_side]; 185 let physicalCorner; 186 // mirror "left-top" to "top-left" etc 187 if (["top", "bottom"].includes(physicalSide1)) { 188 physicalCorner = physicalSide1 + "-" + physicalSide2; 189 } else { 190 physicalCorner = physicalSide2 + "-" + physicalSide1; 191 } 192 physicalProp = group.physical[physicalCorner]; 193 } else { 194 physicalProp = group.physical[writingMode[logicalSide]]; 195 } 196 associated[logicalProp] = physicalProp; 197 associated[physicalProp] = logicalProp; 198 } 199 200 // Test that logical properties are converted to their physical 201 // equivalent correctly when all in the group are present on a single 202 // declaration, with no overwriting of previous properties and 203 // no physical properties present. We put the writing mode properties 204 // on a separate declaration to test that the computed values of these 205 // properties are used, rather than those on the same declaration. 206 test(function() { 207 let decl = group.prerequisites; 208 const expected = []; 209 for (const [i, logicalProp] of logicals.entries()) { 210 decl += `${logicalProp}: ${values[i]}; `; 211 expected.push([logicalProp, values[i]]); 212 expected.push([associated[logicalProp], values[i]]); 213 } 214 testComputedValues("logical properties on one declaration, writing " + 215 `mode properties on another, '${writingModeDecl}'`, 216 `.test { ${writingModeDecl} } .test { ${decl} }`, 217 expected); 218 }, `Test that logical ${group.property} properties share computed values ` 219 + `with their physical associates, with '${writingModeDecl}'.`); 220 221 // Test logical shorthand properties. 222 if (shorthands) { 223 test(function() { 224 for (const [shorthand, longhands] of shorthands) { 225 let valueArray = shorthandValues[shorthand]; 226 const decl = group.prerequisites + `${shorthand}: ${valueArray.join(" ")}; `; 227 const expected = []; 228 for (let [i, longhand] of longhands.entries()) { 229 const longhandValue = valueArray[group.type.length > 1 ? 0 : i]; 230 expected.push([longhand, longhandValue]); 231 expected.push([associated[longhand], longhandValue]); 232 } 233 testComputedValues("shorthand properties on one declaration, writing " + 234 `mode properties on another, '${writingModeDecl}'`, 235 `.test { ${writingModeDecl} } .test { ${decl} }`, 236 expected); 237 } 238 }, `Test that ${group.property} shorthands set the computed value of both ` 239 + `logical and physical longhands, with '${writingModeDecl}'.`); 240 } 241 242 // Test that logical and physical properties are cascaded together, 243 // honoring their relative order on a single declaration 244 // (a) with a single logical property after the physical ones 245 // (b) with a single physical property after the logical ones 246 test(function() { 247 for (const lastIsLogical of [true, false]) { 248 const lasts = lastIsLogical ? logicals : physicals; 249 const others = lastIsLogical ? physicals : logicals; 250 for (const lastProp of lasts) { 251 let decl = writingModeDecl + group.prerequisites; 252 const expected = []; 253 for (const [i, prop] of others.entries()) { 254 decl += `${prop}: ${values[i]}; `; 255 const valueIdx = associated[prop] === lastProp ? others.length : i; 256 expected.push([prop, values[valueIdx]]); 257 expected.push([associated[prop], values[valueIdx]]); 258 } 259 decl += `${lastProp}: ${values[others.length]}; `; 260 testComputedValues(`'${lastProp}' last on single declaration, '${writingModeDecl}'`, 261 `.test { ${decl} }`, 262 expected); 263 } 264 } 265 }, `Test that ${group.property} properties honor order of appearance when both ` 266 + `logical and physical associates are declared, with '${writingModeDecl}'.`); 267 268 // Test that logical and physical properties are cascaded properly when 269 // on different declarations 270 // (a) with a logical property in the high specificity rule 271 // (b) with a physical property in the high specificity rule 272 test(function() { 273 for (const highIsLogical of [true, false]) { 274 let lowDecl = writingModeDecl + group.prerequisites; 275 const high = highIsLogical ? logicals : physicals; 276 const others = highIsLogical ? physicals : logicals; 277 for (const [i, prop] of others.entries()) { 278 lowDecl += `${prop}: ${values[i]}; `; 279 } 280 for (const highProp of high) { 281 const highDecl = `${highProp}: ${values[others.length]}; `; 282 const expected = []; 283 for (const [i, prop] of others.entries()) { 284 const valueIdx = associated[prop] === highProp ? others.length : i; 285 expected.push([prop, values[valueIdx]]); 286 expected.push([associated[prop], values[valueIdx]]); 287 } 288 testComputedValues(`'${highProp}', two declarations, '${writingModeDecl}'`, 289 `#test { ${highDecl} } .test { ${lowDecl} }`, 290 expected); 291 } 292 } 293 }, `Test that ${group.property} properties honor selector specificty when both ` 294 + `logical and physical associates are declared, with '${writingModeDecl}'.`); 295 } 296 } 297 }