From 2fee966fc8f88377e3d8eae0055ba53b72c31b36 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sun, 18 Aug 2019 17:31:07 +0200 Subject: [PATCH] adding test for printer package --- internal/pkg/printer/chrome.go | 112 +++++++++++++++++++------- internal/pkg/printer/html_test.go | 52 ++++++++++++ internal/pkg/printer/markdown.go | 2 +- internal/pkg/printer/markdown_test.go | 55 +++++++++++++ internal/pkg/printer/merge.go | 9 +++ internal/pkg/printer/merge_test.go | 11 ++- internal/pkg/printer/office.go | 10 +++ internal/pkg/printer/office_test.go | 24 ++---- internal/pkg/printer/url_test.go | 52 ++++++++++++ test/multipartform.go | 52 +++++++----- test/testdata.go | 46 ++++++++++- 11 files changed, 346 insertions(+), 79 deletions(-) create mode 100644 internal/pkg/printer/html_test.go create mode 100644 internal/pkg/printer/markdown_test.go create mode 100644 internal/pkg/printer/url_test.go diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index afb3eeea..61b6911c 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -12,6 +12,7 @@ import ( "github.com/mafredri/cdp/protocol/page" "github.com/mafredri/cdp/protocol/target" "github.com/mafredri/cdp/rpcc" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" @@ -26,7 +27,7 @@ type chromePrinter struct { } // ChromePrinterOptions helps customizing the -// Google Chrome printer behaviour. +// Google Chrome Printer behaviour. type ChromePrinterOptions struct { WaitTimeout float64 WaitDelay float64 @@ -41,6 +42,25 @@ type ChromePrinterOptions struct { Landscape bool } +// DefaultChromePrinterOptions returns the default +// Google Chrome Printer options. +func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions { + const defaultHeaderFooterHTML string = "" + return ChromePrinterOptions{ + WaitTimeout: config.DefaultWaitTimeout(), + WaitDelay: 0.0, + HeaderHTML: defaultHeaderFooterHTML, + FooterHTML: defaultHeaderFooterHTML, + PaperWidth: 8.27, + PaperHeight: 11.7, + MarginTop: 1.0, + MarginBottom: 1.0, + MarginLeft: 1.0, + MarginRight: 1.0, + Landscape: false, + } +} + func (p chromePrinter) Print(destination string) error { const op string = "printer.chromePrinter.Print" logOptions(p.logger, p.opts) @@ -83,21 +103,23 @@ func (p chromePrinter) Print(destination string) error { closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID) // close the target when done. defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck - if err := runBatch( - // enable all the domain events that we're interested in. - func() error { return targetClient.DOM.Enable(ctx) }, - func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) }, - func() error { return targetClient.Page.Enable(ctx) }, - func() error { - return targetClient.Page.SetLifecycleEventsEnabled(ctx, page.NewSetLifecycleEventsEnabledArgs(true)) - }, - func() error { return targetClient.Runtime.Enable(ctx) }, - ); err != nil { + // enable all events. + if err := p.enableEvents(ctx, targetClient); err != nil { return err } - if err := p.navigate(ctx, targetClient); err != nil { + // listen for all events. + if err := p.listenEvents(ctx, targetClient); err != nil { return err } + // apply a wait delay (if any). + if p.opts.WaitDelay > 0.0 { + // wait for a given amount of time (useful for javascript delay). + p.logger.DebugfOp(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay) + time.Sleep(xtime.Duration(p.opts.WaitDelay)) + } else { + p.logger.DebugOp(op, "no wait delay to apply, moving on...") + } + // print the page to PDF. print, err := targetClient.Page.PrintToPDF( ctx, page.NewPrintToPDFArgs(). @@ -130,8 +152,25 @@ func (p chromePrinter) Print(destination string) error { return nil } -func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error { - const op string = "printer.chromePrinter.navigate" +func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) error { + const op string = "printer.chromePrinter.enableEvents" + // enable all the domain events that we're interested in. + if err := runBatch( + func() error { return client.DOM.Enable(ctx) }, + func() error { return client.Network.Enable(ctx, network.NewEnableArgs()) }, + func() error { return client.Page.Enable(ctx) }, + func() error { + return client.Page.SetLifecycleEventsEnabled(ctx, page.NewSetLifecycleEventsEnabledArgs(true)) + }, + func() error { return client.Runtime.Enable(ctx) }, + ); err != nil { + return xerror.New(op, err) + } + return nil +} + +func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) error { + const op string = "printer.chromePrinter.listenEvents" resolver := func() error { // make sure Page events are enabled. if err := client.Page.Enable(ctx); err != nil { @@ -165,10 +204,24 @@ func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error { if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil { return err } - if err := runBatch( - // wait for all events. - func() error { _, err := domContentEventFired.Recv(); return err }, - func() error { _, err := loadEventFired.Recv(); return err }, + // wait for all events. + return runBatch( + func() error { + _, err := domContentEventFired.Recv() + if err != nil { + return err + } + p.logger.DebugOp(op, "event 'domContentEventFired' received") + return nil + }, + func() error { + _, err := loadEventFired.Recv() + if err != nil { + return err + } + p.logger.DebugOp(op, "event 'loadEventFired' received") + return nil + }, func() error { const networkIdleEventName string = "networkIdle" for { @@ -176,25 +229,22 @@ func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error { if err != nil { return err } + p.logger.DebugfOp(op, "event '%s' received", ev.Name) if ev.Name == networkIdleEventName { break } } return nil }, - func() error { _, err := loadingFinished.Recv(); return err }, - ); err != nil { - return err - } - - if p.opts.WaitDelay > 0.0 { - // wait for a given amount of time (useful for javascript delay). - p.logger.DebugfOp(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay) - time.Sleep(xtime.Duration(p.opts.WaitDelay)) - } else { - p.logger.DebugOp(op, "no wait delay to apply, moving on...") - } - return nil + func() error { + _, err := loadingFinished.Recv() + if err != nil { + return err + } + p.logger.DebugOp(op, "event 'loadingFinished' received") + return nil + }, + ) } if err := resolver(); err != nil { return xerror.New(op, err) diff --git a/internal/pkg/printer/html_test.go b/internal/pkg/printer/html_test.go new file mode 100644 index 00000000..4a233163 --- /dev/null +++ b/internal/pkg/printer/html_test.go @@ -0,0 +1,52 @@ +package printer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" + "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" + "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" + "github.com/thecodingmachine/gotenberg/test" +) + +func TestHTMLPrinter(t *testing.T) { + var ( + logger xlog.Logger = test.DebugLogger() + config conf.Config = conf.DefaultConfig() + fpath string = test.HTMLFpaths(t)[0] + opts ChromePrinterOptions + dest string + p Printer + err error + ) + // default options. + opts = DefaultChromePrinterOptions(config) + p = NewHTMLPrinter(logger, fpath, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // options with a wait delay. + opts = DefaultChromePrinterOptions(config) + opts.WaitDelay = 0.5 + p = NewHTMLPrinter(logger, fpath, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as context.Context + // should timeout. + opts = DefaultChromePrinterOptions(config) + opts.WaitTimeout = 0.0 + p = NewHTMLPrinter(logger, fpath, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) +} diff --git a/internal/pkg/printer/markdown.go b/internal/pkg/printer/markdown.go index 2b6e9ade..3ac9956c 100644 --- a/internal/pkg/printer/markdown.go +++ b/internal/pkg/printer/markdown.go @@ -35,7 +35,7 @@ func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOpti } baseFilename := xrand.Get() dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename) - logger.DebugOp(op, "writing the HTML from previous conversion into new file...") + logger.DebugOp(op, "writing the HTML from previous conversion(s) into new file...") if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil { return "", err } diff --git a/internal/pkg/printer/markdown_test.go b/internal/pkg/printer/markdown_test.go new file mode 100644 index 00000000..c5115a3a --- /dev/null +++ b/internal/pkg/printer/markdown_test.go @@ -0,0 +1,55 @@ +package printer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" + "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" + "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" + "github.com/thecodingmachine/gotenberg/test" +) + +func TestMarkdownPrinter(t *testing.T) { + var ( + logger xlog.Logger = test.DebugLogger() + config conf.Config = conf.DefaultConfig() + fpath string = test.MarkdownFpaths(t)[0] + opts ChromePrinterOptions + dest string + p Printer + err error + ) + // default options. + opts = DefaultChromePrinterOptions(config) + p, err = NewMarkdownPrinter(logger, fpath, opts) + assert.Nil(t, err) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // options with a wait delay. + opts = DefaultChromePrinterOptions(config) + opts.WaitDelay = 0.5 + p, err = NewMarkdownPrinter(logger, fpath, opts) + assert.Nil(t, err) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as context.Context + // should timeout. + opts = DefaultChromePrinterOptions(config) + opts.WaitTimeout = 0.0 + p, err = NewMarkdownPrinter(logger, fpath, opts) + assert.Nil(t, err) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) +} diff --git a/internal/pkg/printer/merge.go b/internal/pkg/printer/merge.go index d9fd87ad..126301bb 100644 --- a/internal/pkg/printer/merge.go +++ b/internal/pkg/printer/merge.go @@ -3,6 +3,7 @@ package printer import ( "context" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" @@ -22,6 +23,14 @@ type MergePrinterOptions struct { WaitTimeout float64 } +// DefaultMergePrinterOptions returns the default +// merge Printer options. +func DefaultMergePrinterOptions(config conf.Config) MergePrinterOptions { + return MergePrinterOptions{ + WaitTimeout: config.DefaultWaitTimeout(), + } +} + // NewMergePrinter returns a Printer which // is able to merge PDFs. func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer { diff --git a/internal/pkg/printer/merge_test.go b/internal/pkg/printer/merge_test.go index bef9ea46..71399a32 100644 --- a/internal/pkg/printer/merge_test.go +++ b/internal/pkg/printer/merge_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/test" @@ -13,6 +14,7 @@ import ( func TestMergePrinter(t *testing.T) { var ( logger xlog.Logger = test.DebugLogger() + config conf.Config = conf.DefaultConfig() fpaths []string = test.MergeFpaths(t) opts MergePrinterOptions dest string @@ -20,9 +22,7 @@ func TestMergePrinter(t *testing.T) { err error ) // default options. - opts = MergePrinterOptions{ - WaitTimeout: 10.0, - } + opts = DefaultMergePrinterOptions(config) p = NewMergePrinter(logger, fpaths, opts) dest = test.GenerateDestination() err = p.Print(dest) @@ -31,9 +31,8 @@ func TestMergePrinter(t *testing.T) { assert.Nil(t, err) // should not be OK as context.Context // should timeout. - opts = MergePrinterOptions{ - WaitTimeout: 0.0, - } + opts = DefaultMergePrinterOptions(config) + opts.WaitTimeout = 0.0 p = NewMergePrinter(logger, fpaths, opts) dest = test.GenerateDestination() err = p.Print(dest) diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index fb0d8f27..b73a6c13 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" @@ -26,6 +27,15 @@ type OfficePrinterOptions struct { Landscape bool } +// DefaultOfficePrinterOptions returns the default +// Office Printer options. +func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions { + return OfficePrinterOptions{ + WaitTimeout: config.DefaultWaitTimeout(), + Landscape: false, + } +} + // NewOfficePrinter returns a Printer which // is able to convert Office documents to PDF. func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer { diff --git a/internal/pkg/printer/office_test.go b/internal/pkg/printer/office_test.go index dd03f11c..b6a0235b 100644 --- a/internal/pkg/printer/office_test.go +++ b/internal/pkg/printer/office_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/test" @@ -13,6 +14,7 @@ import ( func TestOfficePrinter(t *testing.T) { var ( logger xlog.Logger = test.DebugLogger() + config conf.Config = conf.DefaultConfig() fpaths []string = test.OfficeFpaths(t) opts OfficePrinterOptions dest string @@ -20,10 +22,7 @@ func TestOfficePrinter(t *testing.T) { err error ) // default options. - opts = OfficePrinterOptions{ - WaitTimeout: 10.0, - Landscape: false, - } + opts = DefaultOfficePrinterOptions(config) p = NewOfficePrinter(logger, fpaths, opts) dest = test.GenerateDestination() err = p.Print(dest) @@ -31,10 +30,7 @@ func TestOfficePrinter(t *testing.T) { err = os.RemoveAll(dest) assert.Nil(t, err) // using one file. - opts = OfficePrinterOptions{ - WaitTimeout: 10.0, - Landscape: false, - } + opts = DefaultOfficePrinterOptions(config) p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) dest = test.GenerateDestination() err = p.Print(dest) @@ -42,10 +38,8 @@ func TestOfficePrinter(t *testing.T) { err = os.RemoveAll(dest) assert.Nil(t, err) // options with landscape. - opts = OfficePrinterOptions{ - WaitTimeout: 10.0, - Landscape: true, - } + opts = DefaultOfficePrinterOptions(config) + opts.Landscape = true p = NewOfficePrinter(logger, fpaths, opts) dest = test.GenerateDestination() err = p.Print(dest) @@ -54,10 +48,8 @@ func TestOfficePrinter(t *testing.T) { assert.Nil(t, err) // should not be OK as context.Context // should timeout. - opts = OfficePrinterOptions{ - WaitTimeout: 0.0, - Landscape: true, - } + opts = DefaultOfficePrinterOptions(config) + opts.WaitTimeout = 0.0 p = NewOfficePrinter(logger, fpaths, opts) dest = test.GenerateDestination() err = p.Print(dest) diff --git a/internal/pkg/printer/url_test.go b/internal/pkg/printer/url_test.go new file mode 100644 index 00000000..2c0b99f7 --- /dev/null +++ b/internal/pkg/printer/url_test.go @@ -0,0 +1,52 @@ +package printer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thecodingmachine/gotenberg/internal/pkg/conf" + "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" + "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" + "github.com/thecodingmachine/gotenberg/test" +) + +func TestURLPrinter(t *testing.T) { + var ( + logger xlog.Logger = test.DebugLogger() + config conf.Config = conf.DefaultConfig() + URL = "https://google.com" + opts ChromePrinterOptions + dest string + p Printer + err error + ) + // default options. + opts = DefaultChromePrinterOptions(config) + p = NewURLPrinter(logger, URL, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // options with a wait delay. + opts = DefaultChromePrinterOptions(config) + opts.WaitDelay = 0.5 + p = NewURLPrinter(logger, URL, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as context.Context + // should timeout. + opts = DefaultChromePrinterOptions(config) + opts.WaitTimeout = 0.0 + p = NewURLPrinter(logger, URL, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) +} diff --git a/test/multipartform.go b/test/multipartform.go index b548ee98..0bc73508 100644 --- a/test/multipartform.go +++ b/test/multipartform.go @@ -11,41 +11,51 @@ import ( "github.com/stretchr/testify/require" ) -// MergeMultipartForm returns the body -// for a multipart/form-data request with all -// files under "pdf" folder. +/* +MergeMultipartForm returns the body +for a multipart/form-data request with all +files under "testdata/pdf" folder. +*/ func MergeMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { fpaths := MergeFpaths(t) return multipartForm(t, "pdf", formValues, fpaths) } -// HTMLMultipartForm returns the body -// for a multipart/form-data request with all -// files under "html" folder. +/* +HTMLMultipartForm returns the body +for a multipart/form-data request with all +files under "testdata/html" folder. +*/ func HTMLMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - // TODO - return multipartForm(t, "html", formValues, []string{}) + fpaths := HTMLFpaths(t) + return multipartForm(t, "html", formValues, fpaths) } -// URLMultipartForm returns the body -// for a multipart/form-data request with all -// files under "url" folder. +/* +URLMultipartForm returns the body +for a multipart/form-data request with all +files under "testdata/url" folder. +*/ func URLMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - // TODO - return multipartForm(t, "url", formValues, []string{}) + fpaths := URLFpaths(t) + return multipartForm(t, "url", formValues, fpaths) } -// MarkdownMultipartForm returns the body -// for a multipart/form-data request with all -// files under "markdown" folder. +/* +MarkdownMultipartForm returns the body +for a multipart/form-data request with all +files under "testdata/markdown" folder. +*/ func MarkdownMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - // TODO - return multipartForm(t, "markdown", formValues, []string{}) + fpaths := MarkdownFpaths(t) + return multipartForm(t, "markdown", formValues, fpaths) } -// OfficeMultipartForm returns the body -// for a multipart/form-data request with all -// files under "office" folder. +/* +OfficeMultipartForm returns the body +for a multipart/form-data request with all +files under "testdata/office" folder. +*/ func OfficeMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { fpaths := OfficeFpaths(t) return multipartForm(t, "office", formValues, fpaths) diff --git a/test/testdata.go b/test/testdata.go index cfafcf95..e40ab818 100644 --- a/test/testdata.go +++ b/test/testdata.go @@ -21,8 +21,8 @@ func GenerateDestination() string { return fmt.Sprintf("/tmp/%s.pdf", xrand.Get()) } -// MergeFpaths return the paths -// of the PDF files used in tests. +// MergeFpaths return the paths of all +// files under "testdata/pdf" folder. func MergeFpaths(t *testing.T) []string { return []string{ fpath(t, "pdf", "gotenberg.pdf"), @@ -30,8 +30,46 @@ func MergeFpaths(t *testing.T) []string { } } -// OfficeFpaths return the paths -// of the Office documents used in tests. +// HTMLFpaths return the paths of all +// files under "testdata/html" folder. +func HTMLFpaths(t *testing.T) []string { + return []string{ + fpath(t, "html", "index.html"), + fpath(t, "html", "header.html"), + fpath(t, "html", "footer.html"), + fpath(t, "html", "style.css"), + fpath(t, "html", "img.gif"), + fpath(t, "html", "font.woff"), + } +} + +// URLFpaths return the paths of all +// files under "testdata/url" folder. +func URLFpaths(t *testing.T) []string { + return []string{ + fpath(t, "url", "header.html"), + fpath(t, "url", "footer.html"), + } +} + +// MarkdownFpaths return the paths of all +// files under "testdata/markdown" folder. +func MarkdownFpaths(t *testing.T) []string { + return []string{ + fpath(t, "markdown", "index.html"), + fpath(t, "markdown", "header.html"), + fpath(t, "markdown", "footer.html"), + fpath(t, "markdown", "style.css"), + fpath(t, "markdown", "img.gif"), + fpath(t, "markdown", "font.woff"), + fpath(t, "markdown", "paragraph1.md"), + fpath(t, "markdown", "paragraph2.md"), + fpath(t, "markdown", "paragraph3.md"), + } +} + +// OfficeFpaths return the paths of all +// files under "testdata/office" folder. func OfficeFpaths(t *testing.T) []string { return []string{ fpath(t, "office", "document.docx"),