import json import os from pathlib import Path import sys import subprocess import unittest from lxml import etree as ET sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import test_expense_template as fixtures from expense_preview import prepare_preview from expense_template import Template, encoded, tag from spreadsheet_layout import formatted, read_layout class SpreadsheetLayoutTests(unittest.TestCase): def setUp(self): self.fixture = fixtures.ExpenseTemplateTests() self.fixture.setUp() self.addCleanup(self.fixture.tearDown) def test_fresh_import_does_not_read_system_mime_database(self): code = """ import sys def audit(event, args): if event == 'open' and str(args[0]).endswith('mime.types'): raise PermissionError('System MIME database is unavailable in the app sandbox') sys.addaudithook(audit) import spreadsheet_layout """ result = subprocess.run([sys.executable, '-c', code], cwd=Path(__file__).resolve().parents[1], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) def test_actual_workbook_geometry_styles_and_values(self): output, _ = self.fixture.export(payee=dict(recipient='测试收款人', bankName='测试银行测试支行', accountNumber='001234567890')) result = prepare_preview(output) before = output.read_bytes() layout = read_layout(output) self.assertEqual(before, output.read_bytes()) self.assertEqual(layout, result['previewLayout']) sheet = layout['sheets'][0] cells = {cell['reference']: cell for cell in sheet['cells']} self.assertEqual(cells['C23']['text'], '测试收款人') self.assertEqual(cells['C25']['text'], '001234567890') self.assertEqual(cells['F27']['text'].strip(), '544.30') self.assertEqual(cells['D29']['text'], '伍佰肆拾肆元叁角整') self.assertNotIn('D23', cells) self.assertAlmostEqual(cells['C23']['x'] + cells['C23']['width'], sheet['width']) self.assertEqual(cells['H30']['style']['rotation'], 255) self.assertGreater(cells['A30']['overflowRight'], cells['A30']['x'] + cells['A30']['width']) self.assertLessEqual(cells['A30']['overflowRight'], cells['D30']['x']) _, _, document = Template(output).sheet(sheet['name']) row = document.find(tag('sheetData') + '/' + tag('row') + '[@r="30"]') self.assertAlmostEqual(cells['A30']['height'], float(row.get('ht')) * 4 / 3) self.assertAlmostEqual(cells['H30']['height'], sum(cells[ref]['height'] for ref in ['A30', 'A31', 'A32'])) self.assertEqual(cells['F27']['style']['horizontal'], 'center') self.assertEqual(cells['F27']['style']['font'], 'SimSun-ExtB') self.assertEqual(cells['C23']['style']['borders']['right']['style'], 'medium') self.assertLess(sheet['width'], 1200) self.assertGreater(sheet['height'], 1200) if os.environ.get('RECEIPT_PREVIEW_FIXTURE_DIR'): folder = Path(os.environ['RECEIPT_PREVIEW_FIXTURE_DIR']) folder.mkdir(parents=True, exist_ok=True) (folder / 'layout.json').write_text(json.dumps(layout, ensure_ascii=False), encoding='utf-8') (folder / 'preview.xlsx').write_bytes(before) def test_all_visible_sheets_and_pagination_remain_available(self): template, mapping = self.fixture.alternate() state = dict(matches=self.fixture.state['matches'] + [self.fixture.match('third', '住宿', '50')]) output, _ = self.fixture.export(mapping=mapping, template=template, state=state) layout = prepare_preview(output)['previewLayout'] self.assertEqual([sheet['name'] for sheet in layout['sheets']], ['组 B 报销', '保留说明', '组 B 报销-续2']) notes = layout['sheets'][1] self.assertEqual(notes['cells'][0]['text'], '保持这个工作表不变') self.assertTrue(all(sheet['width'] > 0 and sheet['height'] > 0 for sheet in layout['sheets'])) def test_hidden_rows_columns_and_cells_outside_print_area(self): output, _ = self.fixture.export() template = Template(output) _, path, document = template.sheet('个人报销单') document.find(tag('cols'))[0].set('hidden', '1') row = document.find(tag('sheetData'))[0] row.set('hidden', '1') from expense_template import set_cell set_cell(document, 'J36', '不能遗漏打印区域以外的内容') template.parts[path] = encoded(document) self.fixture.rewrite(output, template.parts) sheet = read_layout(output)['sheets'][0] cells = {cell['reference']: cell for cell in sheet['cells']} self.assertNotIn('A9', cells) self.assertNotIn('A1', cells) self.assertEqual(cells['B9']['x'], 0) self.assertIn('J36', cells) def test_oversized_range_fails_instead_of_truncating(self): output, _ = self.fixture.export() template = Template(output) _, path, document = template.sheet('个人报销单') from expense_template import set_cell set_cell(document, 'IV2000', 'too large') template.parts[path] = encoded(document) self.fixture.rewrite(output, template.parts) with self.assertRaisesRegex(ValueError, '范围过大'): read_layout(output) def test_drawings_and_conditional_formatting_have_explicit_warning(self): output, _ = self.fixture.export() template = Template(output) _, path, document = template.sheet('个人报销单') ET.SubElement(document, tag('conditionalFormatting'), sqref='H9') template.parts[path] = encoded(document) self.fixture.rewrite(output, template.parts) self.assertTrue(read_layout(output)['warnings']) def test_number_formats_and_text_identity(self): warnings = set() for value, kind, code, expected in [ ('2122.60', 'n', '#,##0.00', '2,122.60'), ('662.8', 'n', '0.00_ ', '662.80'), ('0', 'n', '#,##0.00;(#,##0.00);"-"', '-'), ('-25.3', 'n', '#,##0.00;(#,##0.00)', '(25.30)'), ('0.125', 'n', '0.0%', '12.5%'), ('8', 'n', '0000', '0008'), ('80', 'n', '"¥"#,##0.00', '¥80.00'), ('001234567890123456789', 'inlineStr', 'General', '001234567890123456789'), ('=NotAFormula', 'inlineStr', 'General', '=NotAFormula'), ('1', 'b', 'General', 'TRUE'), ('12.50', 'n', 'General', '12.5'), ('12.50', 'n', '0.##', '12.5'), ('12', 'n', '0.0#', '12.0'), ('1', 'n', 'mm-dd-yy', '01-01-00'), ]: with self.subTest(value=value, code=code): self.assertEqual(formatted(value, kind, code, False, warnings), expected) self.assertEqual(formatted('1', 'n', 'yyyy/m/d', True, warnings), '1904/1/2') self.assertFalse(warnings) if __name__ == '__main__': unittest.main()