summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/css-logical/resources/test-box-properties.js
blob: ef1854f97de4c93c3156540ed81101fcc9993578 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import {
  testElement,
  writingModes,
  testCSSValues,
  testComputedValues,
  makeDeclaration
} from "./test-shared.js";

// Values to use while testing
const testValues = {
  "length": ["1px", "2px", "3px", "4px", "5px"],
  "color": ["rgb(1, 1, 1)", "rgb(2, 2, 2)", "rgb(3, 3, 3)", "rgb(4, 4, 4)", "rgb(5, 5, 5)"],
  "border-style": ["solid", "dashed", "dotted", "double", "groove"],
};

/**
 * Creates a group of physical and logical box properties, such as
 *
 * { physical: {
 *     left: "margin-left", right: "margin-right",
 *     top: "margin-top", bottom: "margin-bottom",
 *   }, logical: {
 *     inlineStart: "margin-inline-start", inlineEnd: "margin-inline-end",
 *     blockStart: "margin-block-start", blockEnd: "margin-block-end",
 *   }, shorthands: {
 *     "margin": ["margin-top", "margin-right", "margin-bottom", "margin-left"],
 *     "margin-inline": ["margin-inline-start", "margin-inline-end"],
 *     "margin-block": ["margin-block-start", "margin-block-end"],
 *   }, type: ["length"], prerequisites: "...", property: "margin-*" }
 *
 * @param {string} property
 *        A string representing the property names, like "margin-*".
 * @param {Object} descriptor
 * @param {string|string[]} descriptor.type
 *        Describes the kind of values accepted by the property, like "length".
 *        Must be a key or a collection of keys from the `testValues` object.
 * @param {Object={}} descriptor.prerequisites
 *        Represents property declarations that are needed by `property` to work.
 *        For example, border-width properties require a border style.
 */
export function createBoxPropertyGroup(property, descriptor) {
  const logical = {};
  const physical = {};
  const shorthands = {};
  for (const axis of ["inline", "block"]) {
    const shorthand = property.replace("*", axis);
    const longhands = [];
    shorthands[shorthand] = longhands;
    for (const side of ["start", "end"]) {
      const logicalSide = axis + "-" + side;
      const camelCase = logicalSide.replace(/-(.)/g, (match, $1) => $1.toUpperCase());
      const longhand = property.replace("*", logicalSide);
      logical[camelCase] = longhand;
      longhands.push(longhand);
    }
  }
  const isInset = property === "inset-*";
  let prerequisites = "";
  for (const physicalSide of ["left", "right", "top", "bottom"]) {
    physical[physicalSide] = isInset ? physicalSide : property.replace("*", physicalSide);
    prerequisites += makeDeclaration(descriptor.prerequisites, physicalSide);
  }
  shorthands[property.replace("-*", "")] =
    ["top", "right", "bottom", "left"].map(physicalSide => physical[physicalSide]);
  const type = [].concat(descriptor.type);
  return {logical, physical, shorthands, type, prerequisites, property};
}

/**
 * Creates a group physical and logical box-corner properties.
 *
 * @param {string} property
 *        A string representing the property names, like "border-*-radius".
 * @param {Object} descriptor
 * @param {string|string[]} descriptor.type
 *        Describes the kind of values accepted by the property, like "length".
 *        Must be a key or a collection of keys from the `testValues` object.
 * @param {Object={}} descriptor.prerequisites
 *        Represents property declarations that are needed by `property` to work.
 *        For example, border-width properties require a border style.
 */
export function createCornerPropertyGroup(property, descriptor) {
  const logical = {};
  const physical = {};
  const shorthands = {};
  for (const logicalCorner of ["start-start", "start-end", "end-start", "end-end"]) {
    const prop = property.replace("*", logicalCorner);
    const [block_side, inline_side] = logicalCorner.split("-");
    const b = "block" + block_side.charAt(0).toUpperCase() + block_side.slice(1);
    const i = "inline" + inline_side.charAt(0).toUpperCase() + inline_side.slice(1);
    const index = b + "-" + i; // e.g. "blockStart-inlineEnd"
    logical[index] = prop;
  }
  let prerequisites = "";
  for (const physicalCorner of ["top-left", "top-right", "bottom-left", "bottom-right"]) {
    const prop = property.replace("*", physicalCorner);
    physical[physicalCorner] = prop;
    prerequisites += makeDeclaration(descriptor.prerequisites, physicalCorner);
  }
  const type = [].concat(descriptor.type);
  return {logical, physical, shorthands, type, prerequisites, property};
}

/**
 * Creates a group of physical and logical sizing properties.
 *
 * @param {string} prefix
 *        One of "", "max-" or "min-".
 */
export function createSizingPropertyGroup(prefix) {
  return {
    logical: {
      inline: `${prefix}inline-size`,
      block: `${prefix}block-size`,
    },
    physical: {
      horizontal: `${prefix}width`,
      vertical: `${prefix}height`,
    },
    type: ["length"],
    prerequisites: makeDeclaration({display: "block"}),
    property: (prefix ? prefix.slice(0, -1) + " " : "") + "sizing",
  };
}

/**
 * Tests a grup of logical and physical properties in different writing modes.
 *
 * @param {Object} group
 *        An object returned by createBoxPropertyGroup or createSizingPropertyGroup.
 */
export function runTests(group) {
  const values = testValues[group.type[0]].map(function(_, i) {
    return group.type.map(type => testValues[type][i]).join(" ");
  });
  const logicals = Object.values(group.logical);
  const physicals = Object.values(group.physical);
  const shorthands = group.shorthands ? Object.entries(group.shorthands) : null;
  const is_corner = group.property == "border-*-radius";

  test(function() {
    const expected = [];
    for (const [i, logicalProp] of logicals.entries()) {
      testElement.style.setProperty(logicalProp, values[i]);
      expected.push([logicalProp, values[i]]);
    }
    testCSSValues("logical properties in inline style", testElement.style, expected);
  }, `Test that logical ${group.property} properties are supported.`);
  testElement.style.cssText = "";

  const shorthandValues = {};
  for (const [shorthand, longhands] of shorthands || []) {
    let valueArray;
    if (group.type.length > 1) {
      valueArray = [values[0]];
    } else {
      valueArray = testValues[group.type].slice(0, longhands.length);
    }
    shorthandValues[shorthand] = valueArray;
    const value = valueArray.join(" ");
    const expected = [[shorthand, value]];
    for (let [i, longhand] of longhands.entries()) {
      expected.push([longhand, valueArray[group.type.length > 1 ? 0 : i]]);
    }
    test(function() {
      testElement.style.setProperty(shorthand, value);
      testCSSValues("shorthand in inline style", testElement.style, expected);
      const stylesheet = `.test { ${group.prerequisites} }`;
      testComputedValues("shorthand in computed style", stylesheet, expected);
    }, `Test that ${shorthand} shorthand sets longhands and serializes correctly.`);
    testElement.style.cssText = "";
  }

  for (const writingMode of writingModes) {
    for (const style of writingMode.styles) {
      const writingModeDecl = makeDeclaration(style);

      const associated = {};
      for (const [logicalSide, logicalProp] of Object.entries(group.logical)) {
        let physicalProp;
        if (is_corner) {
          const [ block_side, inline_side] = logicalSide.split("-");
          const physicalSide1 = writingMode[block_side];
          const physicalSide2 = writingMode[inline_side];
          let physicalCorner;
          // mirror "left-top" to "top-left" etc
          if (["top", "bottom"].includes(physicalSide1)) {
            physicalCorner = physicalSide1 + "-" + physicalSide2;
          } else {
            physicalCorner = physicalSide2 + "-" + physicalSide1;
          }
          physicalProp = group.physical[physicalCorner];
        } else {
          physicalProp = group.physical[writingMode[logicalSide]];
        }
        associated[logicalProp] = physicalProp;
        associated[physicalProp] = logicalProp;
      }

      // Test that logical properties are converted to their physical
      // equivalent correctly when all in the group are present on a single
      // declaration, with no overwriting of previous properties and
      // no physical properties present.  We put the writing mode properties
      // on a separate declaration to test that the computed values of these
      // properties are used, rather than those on the same declaration.
      test(function() {
        let decl = group.prerequisites;
        const expected = [];
        for (const [i, logicalProp] of logicals.entries()) {
          decl += `${logicalProp}: ${values[i]}; `;
          expected.push([logicalProp, values[i]]);
          expected.push([associated[logicalProp], values[i]]);
        }
        testComputedValues("logical properties on one declaration, writing " +
                           `mode properties on another, '${writingModeDecl}'`,
                           `.test { ${writingModeDecl} } .test { ${decl} }`,
                           expected);
      }, `Test that logical ${group.property} properties share computed values `
       + `with their physical associates, with '${writingModeDecl}'.`);

      // Test logical shorthand properties.
      if (shorthands) {
        test(function() {
          for (const [shorthand, longhands] of shorthands) {
            let valueArray = shorthandValues[shorthand];
            const decl = group.prerequisites + `${shorthand}: ${valueArray.join(" ")}; `;
            const expected = [];
            for (let [i, longhand] of longhands.entries()) {
              const longhandValue = valueArray[group.type.length > 1 ? 0 : i];
              expected.push([longhand, longhandValue]);
              expected.push([associated[longhand], longhandValue]);
            }
            testComputedValues("shorthand properties on one declaration, writing " +
                               `mode properties on another, '${writingModeDecl}'`,
                               `.test { ${writingModeDecl} } .test { ${decl} }`,
                               expected);
          }
        }, `Test that ${group.property} shorthands set the computed value of both `
         + `logical and physical longhands, with '${writingModeDecl}'.`);
      }

      // Test that logical and physical properties are cascaded together,
      // honoring their relative order on a single declaration
      // (a) with a single logical property after the physical ones
      // (b) with a single physical property after the logical ones
      test(function() {
        for (const lastIsLogical of [true, false]) {
          const lasts = lastIsLogical ? logicals : physicals;
          const others = lastIsLogical ? physicals : logicals;
          for (const lastProp of lasts) {
            let decl = writingModeDecl + group.prerequisites;
            const expected = [];
            for (const [i, prop] of others.entries()) {
              decl += `${prop}: ${values[i]}; `;
              const valueIdx = associated[prop] === lastProp ? others.length : i;
              expected.push([prop, values[valueIdx]]);
              expected.push([associated[prop], values[valueIdx]]);
            }
            decl += `${lastProp}: ${values[others.length]}; `;
            testComputedValues(`'${lastProp}' last on single declaration, '${writingModeDecl}'`,
                               `.test { ${decl} }`,
                               expected);
          }
        }
      }, `Test that ${group.property} properties honor order of appearance when both `
       + `logical and physical associates are declared, with '${writingModeDecl}'.`);

      // Test that logical and physical properties are cascaded properly when
      // on different declarations
      // (a) with a logical property in the high specificity rule
      // (b) with a physical property in the high specificity rule
      test(function() {
        for (const highIsLogical of [true, false]) {
          let lowDecl = writingModeDecl + group.prerequisites;
          const high = highIsLogical ? logicals : physicals;
          const others = highIsLogical ? physicals : logicals;
          for (const [i, prop] of others.entries()) {
            lowDecl += `${prop}: ${values[i]}; `;
          }
          for (const highProp of high) {
            const highDecl = `${highProp}: ${values[others.length]}; `;
            const expected = [];
            for (const [i, prop] of others.entries()) {
              const valueIdx = associated[prop] === highProp ? others.length : i;
              expected.push([prop, values[valueIdx]]);
              expected.push([associated[prop], values[valueIdx]]);
            }
            testComputedValues(`'${highProp}', two declarations, '${writingModeDecl}'`,
                               `#test { ${highDecl} } .test { ${lowDecl} }`,
                               expected);
          }
        }
      }, `Test that ${group.property} properties honor selector specificty when both `
       + `logical and physical associates are declared, with '${writingModeDecl}'.`);
    }
  }
}