diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/functional/api/buffer_spec.lua | 5 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 9 | ||||
-rw-r--r-- | test/functional/clipboard/autoload/provider/clipboard.vim | 16 | ||||
-rw-r--r-- | test/functional/clipboard/clipboard_provider_spec.lua | 141 | ||||
-rw-r--r-- | test/functional/helpers.lua | 53 | ||||
-rw-r--r-- | test/functional/legacy/026_execute_while_if_spec.lua | 18 | ||||
-rw-r--r-- | test/functional/legacy/033_lisp_indent_spec.lua | 20 | ||||
-rw-r--r-- | test/functional/ui/highlight_spec.lua | 184 | ||||
-rw-r--r-- | test/functional/ui/input_spec.lua | 40 | ||||
-rw-r--r-- | test/functional/ui/mouse_spec.lua | 157 | ||||
-rw-r--r-- | test/functional/ui/screen.lua | 384 | ||||
-rw-r--r-- | test/functional/ui/screen_basic_spec.lua | 224 |
12 files changed, 1197 insertions, 54 deletions
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua index 169d605b63..b85594f7af 100644 --- a/test/functional/api/buffer_spec.lua +++ b/test/functional/api/buffer_spec.lua @@ -34,6 +34,11 @@ describe('buffer_* functions', function() curbuf('del_line', 0) eq('', curbuf('get_line', 0)) end) + + it('can handle NULs', function() + curbuf('set_line', 0, 'ab\0cd') + eq('ab\0cd', curbuf('get_line', 0)) + end) end) diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index f34df8cefb..2c17a2acd0 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -36,6 +36,10 @@ describe('vim_* functions', function() -- 19 * 2 (each japanese character occupies two cells) eq(44, nvim('strwidth', 'neovimのデザインかなりまともなのになってる。')) end) + + it('cannot handle NULs', function() + eq(0, nvim('strwidth', '\0abc')) + end) end) describe('{get,set}_current_line', function() @@ -52,6 +56,11 @@ describe('vim_* functions', function() eq({1, 2, {['3'] = 1}}, nvim('get_var', 'lua')) eq({1, 2, {['3'] = 1}}, nvim('eval', 'g:lua')) end) + + it('truncates values with NULs in them', function() + nvim('set_var', 'xxx', 'ab\0cd') + eq('ab', nvim('get_var', 'xxx')) + end) end) describe('{get,set}_option', function() diff --git a/test/functional/clipboard/autoload/provider/clipboard.vim b/test/functional/clipboard/autoload/provider/clipboard.vim new file mode 100644 index 0000000000..6c05a19fc3 --- /dev/null +++ b/test/functional/clipboard/autoload/provider/clipboard.vim @@ -0,0 +1,16 @@ +let g:test_clip = { '+': [''], '*': [''], } + +let s:methods = {} + +function! s:methods.get(reg) + return g:test_clip[a:reg] +endfunction + +function! s:methods.set(lines, regtype, reg) + let g:test_clip[a:reg] = a:lines +endfunction + + +function! provider#clipboard#Call(method, args) + return call(s:methods[a:method],a:args,s:methods) +endfunction diff --git a/test/functional/clipboard/clipboard_provider_spec.lua b/test/functional/clipboard/clipboard_provider_spec.lua new file mode 100644 index 0000000000..ccbb74e487 --- /dev/null +++ b/test/functional/clipboard/clipboard_provider_spec.lua @@ -0,0 +1,141 @@ +-- Test clipboard provider support + +local helpers = require('test.functional.helpers') +local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert +local execute, expect, eq, eval = helpers.execute, helpers.expect, helpers.eq, helpers.eval +local nvim, run, stop, restart = helpers.nvim, helpers.run, helpers.stop, helpers.restart + +local function reset() + clear() + execute('let &rtp = "test/functional/clipboard,".&rtp') +end + +local function basic_register_test() + insert("some words") + + feed('^dwP') + expect('some words') + + feed('veyP') + expect('some words words') + + feed('^dwywe"-p') + expect('wordssome words') + + feed('p') + expect('wordssome words words') + + feed('yyp') + expect([[ + wordssome words words + wordssome words words]]) + feed('d-') + + insert([[ + some text, and some more + random text stuff]]) + feed('ggtav+2ed$p') + expect([[ + some text, stuff and some more + random text]]) + reset() +end + +describe('clipboard usage', function() + setup(reset) + it("works", function() + basic_register_test() + + -- "* and unnamed should function as independent registers + insert("some words") + feed('^"*dwdw"*P') + expect('some ') + eq({'some '}, eval("g:test_clip['*']")) + reset() + + -- "* and "+ should be independent when the provider supports it + insert([[ + text: + first line + secound line + third line]]) + + feed('G"+dd"*dddd"+p"*pp') + expect([[ + text: + third line + secound line + first line]]) + -- linewise selection should be encoded as an extra newline + eq({'third line', ''}, eval("g:test_clip['+']")) + eq({'secound line', ''}, eval("g:test_clip['*']")) + reset() + + -- handle null bytes + insert("some\x16000text\n\x16000very binary\x16000") + feed('"*y-+"*p') + eq({'some\ntext', '\nvery binary\n',''}, eval("g:test_clip['*']")) + expect("some\x00text\n\x00very binary\x00\nsome\x00text\n\x00very binary\x00") + + -- test getreg/getregtype + eq('some\ntext\n\nvery binary\n\n', eval("getreg('*', 1)")) + eq("V", eval("getregtype('*')")) + reset() + + -- blockwise paste + insert([[ + much + text]]) + feed('"*yy') -- force load of provider + execute("let g:test_clip['*'] = [['very','block'],'b']") + feed('gg"*P') + expect([[ + very much + blocktext]]) + eq("\x165", eval("getregtype('*')")) + reset() + + -- test setreg + execute('call setreg("*", "setted\\ntext", "c")') + execute('call setreg("+", "explicitly\\nlines", "l")') + feed('"+P"*p') + expect([[ + esetted + textxplicitly + lines + ]]) + reset() + + -- test let @+ (issue #1427) + execute("let @+ = 'some'") + execute("let @* = ' other stuff'") + eq({'some'}, eval("g:test_clip['+']")) + eq({' other stuff'}, eval("g:test_clip['*']")) + feed('"+p"*p') + expect('some other stuff') + execute("let @+ .= ' more'") + feed('dd"+p') + expect('some more') + reset() + + -- the basic behavior of unnamed register should be the same + -- even when handled by clipboard provider + execute('set clipboard=unnamed') + basic_register_test() + + -- with cb=unnamed, "* and unnamed will be the same register + execute('set clipboard=unnamed') + insert("some words") + feed('^"*dwdw"*P') + expect('words') + eq({'words'}, eval("g:test_clip['*']")) + + execute("let g:test_clip['*'] = ['linewise stuff','']") + feed('p') + expect([[ + words + linewise stuff]]) + reset() + + end) +end) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index b758817b41..fc699d22a3 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -47,12 +47,6 @@ local function request(method, ...) error(rv[2]) end end - -- Make sure this will only return after all buffered characters have been - -- processed - if not loop_stopped then - -- Except when the loop has been stopped by a notification triggered - -- by the initial request, for example. - end return rv end @@ -70,23 +64,30 @@ local function call_and_stop_on_error(...) return result end -local function run(request_cb, notification_cb, setup_cb) +local function run(request_cb, notification_cb, setup_cb, timeout) + local on_request, on_notification, on_setup - local function on_request(method, args) - return call_and_stop_on_error(request_cb, method, args) + if request_cb then + function on_request(method, args) + return call_and_stop_on_error(request_cb, method, args) + end end - local function on_notification(method, args) - call_and_stop_on_error(notification_cb, method, args) + if notification_cb then + function on_notification(method, args) + call_and_stop_on_error(notification_cb, method, args) + end end - local function on_setup() - call_and_stop_on_error(setup_cb) + if setup_cb then + function on_setup() + call_and_stop_on_error(setup_cb) + end end loop_stopped = false loop_running = true - session:run(on_request, on_notification, on_setup) + session:run(on_request, on_notification, on_setup, timeout) loop_running = false if last_error then local err = last_error @@ -115,15 +116,6 @@ local function nvim_feed(input) end end -local function nvim_replace_termcodes(input) - -- small hack to stop <C-@> from being replaced by the internal - -- representation(which is different and won't work for vim_input) - local temp_replacement = 'CCCCCCCCC@@@@@@@@@@' - input = input:gsub('<[Cc][-]@>', temp_replacement) - local rv = request('vim_replace_termcodes', input, false, true, true) - return rv:gsub(temp_replacement, '\000') -end - local function dedent(str) -- find minimum common indent across lines local indent = nil @@ -148,7 +140,7 @@ end local function feed(...) for _, v in ipairs({...}) do - nvim_feed(nvim_replace_termcodes(dedent(v))) + nvim_feed(dedent(v)) end end @@ -161,7 +153,7 @@ end local function clear() if session then session:request('vim_command', 'qa!') - session._async_session._msgpack_stream._loop:exit() + session:exit() end local loop = Loop.new() local msgpack_stream = MsgpackStream.new(loop) @@ -172,8 +164,11 @@ end local function insert(...) nvim_feed('i') - rawfeed(...) - nvim_feed(nvim_replace_termcodes('<ESC>')) + for _, v in ipairs({...}) do + local escaped = v:gsub('<', '<lt>') + rawfeed(escaped) + end + nvim_feed('<ESC>') end local function execute(...) @@ -182,8 +177,8 @@ local function execute(...) -- not a search command, prefix with colon nvim_feed(':') end - nvim_feed(v) - nvim_feed(nvim_replace_termcodes('<CR>')) + nvim_feed(v:gsub('<', '<lt>')) + nvim_feed('<CR>') end end diff --git a/test/functional/legacy/026_execute_while_if_spec.lua b/test/functional/legacy/026_execute_while_if_spec.lua index 9acbf76673..ffe37819de 100644 --- a/test/functional/legacy/026_execute_while_if_spec.lua +++ b/test/functional/legacy/026_execute_while_if_spec.lua @@ -13,11 +13,7 @@ describe(':execute, :while and :if', function() let i = 0 while i < 12 let i = i + 1 - if has("ebcdic") - execute "normal o" . i . "\047" - else - execute "normal o" . i . "\033" - endif + execute "normal o" . i . "\033" if i % 2 normal Ax if i == 9 @@ -28,21 +24,13 @@ describe(':execute, :while and :if', function() else let j = 9 while j > 0 - if has("ebcdic") - execute "normal" j . "a" . j . "\x27" - else - execute "normal" j . "a" . j . "\x1b" - endif + execute "normal" j . "a" . j . "\x1b" let j = j - 1 endwhile endif endif if i == 9 - if has("ebcdic") - execute "normal Az\047" - else - execute "normal Az\033" - endif + execute "normal Az\033" endif endwhile unlet i j diff --git a/test/functional/legacy/033_lisp_indent_spec.lua b/test/functional/legacy/033_lisp_indent_spec.lua index 3ee248815d..0a5577fad3 100644 --- a/test/functional/legacy/033_lisp_indent_spec.lua +++ b/test/functional/legacy/033_lisp_indent_spec.lua @@ -22,7 +22,7 @@ describe('lisp indent', function() :if-exists :supersede) (let ((,ti ,title)) (as title ,ti) - (with center + (with center (as h2 (string-upcase ,ti))) (brs 3) ,@body)))) @@ -35,7 +35,7 @@ describe('lisp indent', function() ,@body (princ "</a>")))]]) - execute('set lisp expandtab') + execute('set lisp') execute('/^(defun') feed('=G:/^(defun/,$yank A<cr>') @@ -52,15 +52,15 @@ describe('lisp indent', function() (defmacro page (name title &rest body) (let ((ti (gensym))) `(with-open-file (*standard-output* - (html-file ,name) - :direction :output - :if-exists :supersede) + (html-file ,name) + :direction :output + :if-exists :supersede) (let ((,ti ,title)) - (as title ,ti) - (with center - (as h2 (string-upcase ,ti))) - (brs 3) - ,@body)))) + (as title ,ti) + (with center + (as h2 (string-upcase ,ti))) + (brs 3) + ,@body)))) ;;; Utilities for generating links diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua new file mode 100644 index 0000000000..3c55c09f95 --- /dev/null +++ b/test/functional/ui/highlight_spec.lua @@ -0,0 +1,184 @@ +local helpers = require('test.functional.helpers') +local Screen = require('test.functional.ui.screen') +local clear, feed, nvim = helpers.clear, helpers.feed, helpers.nvim +local execute = helpers.execute + +describe('Default highlight groups', function() + -- Test the default attributes for highlight groups shown by the :highlight + -- command + local screen, hlgroup_colors + + setup(function() + hlgroup_colors = { + NonText = nvim('name_to_color', 'Blue'), + Question = nvim('name_to_color', 'SeaGreen') + } + end) + + before_each(function() + clear() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + it('window status bar', function() + screen:set_default_attr_ids({ + [1] = {reverse = true, bold = true}, -- StatusLine + [2] = {reverse = true} -- StatusLineNC + }) + execute('sp', 'vsp', 'vsp') + screen:expect([[ + ^ {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {1:[No Name] }{2:[No Name] [No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + -- navigate to verify that the attributes are properly moved + feed('<c-w>j') + screen:expect([[ + {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] [No Name] [No Name] }| + ^ | + ~ | + ~ | + ~ | + ~ | + {1:[No Name] }| + | + ]]) + -- note that when moving to a window with small width nvim will increase + -- the width of the new active window at the expense of a inactive window + -- (upstream vim has the same behavior) + feed('<c-w>k<c-w>l') + screen:expect([[ + {2:|}^ {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] }{1:[No Name] }{2:[No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + feed('<c-w>l') + screen:expect([[ + {2:|} {2:|}^ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] [No Name] }{1:[No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + feed('<c-w>h<c-w>h') + screen:expect([[ + ^ {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {1:[No Name] }{2:[No Name] [No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + end) + + it('insert mode text', function() + feed('i') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + {1:-- INSERT --} | + ]], {[1] = {bold = true}}) + end) + + it('end of file markers', function() + nvim('command', 'hi Normal guibg=black') + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]], {[1] = {bold = true, foreground = hlgroup_colors.NonText}}) + end) + + it('"wait return" text', function() + feed(':ls<cr>') + screen:expect([[ + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls | + 1 %a "[No Name]" line 1 | + {1:Press ENTER or type command to continue}^ | + ]], {[1] = {bold = true, foreground = hlgroup_colors.Question}}) + feed('<cr>') -- skip the "Press ENTER..." state or tests will hang + end) +end) diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua new file mode 100644 index 0000000000..60a49c4ed7 --- /dev/null +++ b/test/functional/ui/input_spec.lua @@ -0,0 +1,40 @@ +local helpers = require('test.functional.helpers') +local clear, execute, nvim = helpers.clear, helpers.execute, helpers.nvim +local feed, next_message, eq = helpers.feed, helpers.next_message, helpers.eq + +describe('mappings', function() + local cid + + local add_mapping = function(mapping, send) + local str = 'mapped '..mapping + local cmd = "nnoremap "..mapping.." :call rpcnotify("..cid..", 'mapped', '" + ..send:gsub('<', '<lt>').."')<cr>" + execute(cmd) + end + + local check_mapping = function(mapping, expected) + feed(mapping) + eq({'notification', 'mapped', {expected}}, next_message()) + end + + before_each(function() + clear() + cid = nvim('get_api_info')[1] + add_mapping('<s-up>', '<s-up>') + add_mapping('<s-up>', '<s-up>') + add_mapping('<c-s-up>', '<c-s-up>') + add_mapping('<c-s-a-up>', '<c-s-a-up>') + end) + + it('ok', function() + check_mapping('<s-up>', '<s-up>') + check_mapping('<c-s-up>', '<c-s-up>') + check_mapping('<s-c-up>', '<c-s-up>') + check_mapping('<c-s-a-up>', '<c-s-a-up>') + check_mapping('<s-c-a-up>', '<c-s-a-up>') + check_mapping('<c-a-s-up>', '<c-s-a-up>') + check_mapping('<s-a-c-up>', '<c-s-a-up>') + check_mapping('<a-c-s-up>', '<c-s-a-up>') + check_mapping('<a-s-c-up>', '<c-s-a-up>') + end) +end) diff --git a/test/functional/ui/mouse_spec.lua b/test/functional/ui/mouse_spec.lua new file mode 100644 index 0000000000..507b5aacae --- /dev/null +++ b/test/functional/ui/mouse_spec.lua @@ -0,0 +1,157 @@ +local helpers = require('test.functional.helpers') +local Screen = require('test.functional.ui.screen') +local clear, feed, nvim = helpers.clear, helpers.feed, helpers.nvim + +describe('Mouse input', function() + local screen, hlgroup_colors + + setup(function() + hlgroup_colors = { + Visual = nvim('name_to_color', 'LightGrey'), + } + end) + + before_each(function() + clear() + nvim('set_option', 'mouse', 'a') + -- set mouset to very high value to ensure that even in valgrind/travis, + -- nvim will still pick multiple clicks + nvim('set_option', 'mouset', 5000) + screen = Screen.new(25, 5) + screen:attach() + screen:set_default_attr_ids({ + [1] = {background = hlgroup_colors.Visual} + }) + feed('itesting<cr>mouse<cr>support and selection<esc>') + screen:expect([[ + testing | + mouse | + support and selectio^ | + ~ | + | + ]]) + end) + + after_each(function() + screen:detach() + end) + + it('left click moves cursor', function() + feed('<LeftMouse><2,1>') + screen:expect([[ + testing | + mo^se | + support and selection | + ~ | + | + ]]) + feed('<LeftMouse><0,0>') + screen:expect([[ + ^esting | + mouse | + support and selection | + ~ | + | + ]]) + end) + + it('left drag changes visual selection', function() + -- drag events must be preceded by a click + feed('<LeftMouse><2,1>') + screen:expect([[ + testing | + mo^se | + support and selection | + ~ | + | + ]]) + feed('<LeftDrag><4,1>') + screen:expect([[ + testing | + mo{1:us}^ | + support and selection | + ~ | + -- VISUAL -- | + ]]) + feed('<LeftDrag><2,2>') + screen:expect([[ + testing | + mo{1:use } | + {1:su}^port and selection | + ~ | + -- VISUAL -- | + ]]) + feed('<LeftDrag><0,0>') + screen:expect([[ + ^{1:esting } | + {1:mou}se | + support and selection | + ~ | + -- VISUAL -- | + ]]) + end) + + it('two clicks will select the word and enter VISUAL', function() + feed('<LeftMouse><2,2><LeftMouse><2,2>') + screen:expect([[ + testing | + mouse | + {1:suppor}^ and selection | + ~ | + -- VISUAL -- | + ]]) + end) + + it('three clicks will select the line and enter VISUAL LINE', function() + feed('<LeftMouse><2,2><LeftMouse><2,2><LeftMouse><2,2>') + screen:expect([[ + testing | + mouse | + {1:su}^{1:port and selection } | + ~ | + -- VISUAL LINE -- | + ]]) + end) + + it('four clicks will enter VISUAL BLOCK', function() + feed('<LeftMouse><2,2><LeftMouse><2,2><LeftMouse><2,2><LeftMouse><2,2>') + screen:expect([[ + testing | + mouse | + su^port and selection | + ~ | + -- VISUAL BLOCK -- | + ]]) + end) + + it('right click extends visual selection to the clicked location', function() + feed('<LeftMouse><0,0>') + screen:expect([[ + ^esting | + mouse | + support and selection | + ~ | + | + ]]) + feed('<RightMouse><2,2>') + screen:expect([[ + {1:testing } | + {1:mouse } | + {1:su}^port and selection | + ~ | + -- VISUAL -- | + ]]) + end) + + it('ctrl + left click will search for a tag', function() + feed('<C-LeftMouse><0,0>') + screen:expect([[ + E433: No tags file | + E426: tag not found: test| + ing | + Press ENTER or type comma| + nd to continue^ | + ]]) + feed('<cr>') + end) +end) diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua new file mode 100644 index 0000000000..8e7d1ed798 --- /dev/null +++ b/test/functional/ui/screen.lua @@ -0,0 +1,384 @@ +-- This module contains the Screen class, a complete Nvim screen implementation +-- designed for functional testing. The goal is to provide a simple and +-- intuitive API for verifying screen state after a set of actions. +-- +-- The screen class exposes a single assertion method, "Screen:expect". This +-- method takes a string representing the expected screen state and an optional +-- set of attribute identifiers for checking highlighted characters(more on +-- this later). +-- +-- The string passed to "expect" will be processed according to these rules: +-- +-- - Each line of the string represents and is matched individually against +-- a screen row. +-- - The entire string is stripped of common indentation +-- - Expected screen rows are stripped of the last character. The last +-- character should be used to write pipes(|) that make clear where the +-- screen ends +-- - The last line is stripped, so the string must have (row count + 1) +-- lines. +-- +-- Example usage: +-- +-- local screen = Screen.new(25, 10) +-- -- attach the screen to the current Nvim instance +-- screen:attach() +-- --enter insert mode and type some text +-- feed('ihello screen') +-- -- declare an expectation for the eventual screen state +-- screen:expect([[ +-- hello screen | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- -- INSERT -- | +-- ]]) -- <- Last line is stripped +-- +-- Since screen updates are received asynchronously, "expect" is actually +-- specifying the eventual screen state. This is how "expect" works: It will +-- start the event loop with a timeout of 5 seconds. Each time it receives an +-- update the expected state will be checked against the updated state. +-- +-- If the expected state matches the current state, the event loop will be +-- stopped and "expect" will return. If the timeout expires, the last match +-- error will be reported and the test will fail. +-- +-- If the second argument is passed to "expect", the screen rows will be +-- transformed before being matched against the string lines. The +-- transformation rule is simple: Each substring "S" composed with characters +-- having the exact same set of attributes will be substituted by "{K:S}", +-- where K is a key associated the attribute set via the second argument of +-- "expect". +-- +-- Too illustrate how this works, let's say that in the above example we wanted +-- to assert that the "-- INSERT --" string is highlighted with the bold +-- attribute(which normally is), here's how the call to "expect" should look +-- like: +-- +-- screen:expect([[ +-- hello screen \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- {b:-- INSERT --} \ +-- ]], {b = {bold = true}}) +-- +-- In this case "b" is a string associated with the set composed of one +-- attribute: bold. Note that since the {b:} markup is not a real part of the +-- screen, the delimiter(|) had to be moved right +local helpers = require('test.functional.helpers') +local request, run, stop = helpers.request, helpers.run, helpers.stop +local eq, dedent = helpers.eq, helpers.dedent + +local Screen = {} +Screen.__index = Screen + +function Screen.new(width, height) + if not width then + width = 53 + end + if not height then + height = 14 + end + return setmetatable({ + _default_attr_ids = nil, + _width = width, + _height = height, + _rows = new_cell_grid(width, height), + _mode = 'normal', + _mouse_enabled = true, + _bell = false, + _visual_bell = false, + _suspended = true, + _attrs = {}, + _cursor = { + enabled = true, row = 1, col = 1 + }, + _scroll_region = { + top = 1, bot = height, left = 1, right = width + } + }, Screen) +end + +function Screen:set_default_attr_ids(attr_ids) + self._default_attr_ids = attr_ids +end + +function Screen:attach() + request('attach_ui', self._width, self._height) + self._suspended = false +end + +function Screen:detach() + request('detach_ui') + self._suspended = true +end + +function Screen:expect(expected, attr_ids) + -- remove the last line and dedent + expected = dedent(expected:gsub('\n[ ]+$', '')) + local expected_rows = {} + for row in expected:gmatch('[^\n]+') do + -- the last character should be the screen delimiter + row = row:sub(1, #row - 1) + table.insert(expected_rows, row) + end + local ids = attr_ids or self._default_attr_ids + self:_wait(function() + for i = 1, self._height do + local expected_row = expected_rows[i] + local actual_row = self:_row_repr(self._rows[i], ids) + if expected_row ~= actual_row then + return 'Row '..tostring(i)..' didnt match.\nExpected: "'.. + expected_row..'"\nActual: "'..actual_row..'"' + end + end + end) +end + +function Screen:_wait(check, timeout) + local err, checked = false + local function notification_cb(method, args) + assert(method == 'redraw') + self:_redraw(args) + err = check() + checked = true + if not err then + stop() + end + return true + end + run(nil, notification_cb, nil, timeout or 5000) + if not checked then + err = check() + end + if err then + error(err) + end +end + +function Screen:_redraw(updates) + for _, update in ipairs(updates) do + -- print('--') + -- print(require('inspect')(update)) + local method = update[1] + for i = 2, #update do + local handler = self['_handle_'..method] + handler(self, unpack(update[i])) + end + -- print(self:_current_screen()) + end +end + +function Screen:_handle_resize(width, height) + self._rows = new_cell_grid(width, height) +end + +function Screen:_handle_clear() + self:_clear_block(1, self._height, 1, self._width) +end + +function Screen:_handle_eol_clear() + local row, col = self._cursor.row, self._cursor.col + self:_clear_block(row, 1, col, self._width - col) +end + +function Screen:_handle_cursor_goto(row, col) + self._cursor.row = row + 1 + self._cursor.col = col + 1 +end + +function Screen:_handle_cursor_on() + self._cursor.enabled = true +end + +function Screen:_handle_cursor_off() + self._cursor.enabled = false +end + +function Screen:_handle_mouse_on() + self._mouse_enabled = true +end + +function Screen:_handle_mouse_off() + self._mouse_enabled = false +end + +function Screen:_handle_insert_mode() + self._mode = 'insert' +end + +function Screen:_handle_normal_mode() + self._mode = 'normal' +end + +function Screen:_handle_set_scroll_region(top, bot, left, right) + self._scroll_region.top = top + 1 + self._scroll_region.bot = bot + 1 + self._scroll_region.left = left + 1 + self._scroll_region.right = right + 1 +end + +function Screen:_handle_scroll(count) + local top = self._scroll_region.top + local bot = self._scroll_region.bot + local left = self._scroll_region.left + local right = self._scroll_region.right + local start, stop, step + + if count > 0 then + start = top + stop = bot - count + step = 1 + else + start = bot + stop = top - count + step = -1 + end + + -- shift scroll region + for i = start, stop, step do + local target = self._rows[i] + local source = self._rows[i + count] + self:_copy_row_section(target, source, left, right) + end + + -- clear invalid rows + for i = stop + 1, stop + count, step do + self:_clear_row_section(i, left, right) + end +end + +function Screen:_handle_highlight_set(attrs) + self._attrs = attrs +end + +function Screen:_handle_put(str) + local cell = self._rows[self._cursor.row][self._cursor.col] + cell.text = str + cell.attrs = self._attrs + self._cursor.col = self._cursor.col + 1 +end + +function Screen:_handle_bell() + self._bell = true +end + +function Screen:_handle_visual_bell() + self._visual_bell = true +end + +function Screen:_handle_suspend() + self._suspended = true +end + +function Screen:_clear_block(top, lines, left, columns) + for i = top, top + lines - 1 do + self:_clear_row_section(i, left, left + columns - 1) + end +end + +function Screen:_clear_row_section(rownum, startcol, stopcol) + local row = self._rows[rownum] + for i = startcol, stopcol do + row[i].text = ' ' + row[i].attrs = {} + end +end + +function Screen:_copy_row_section(target, source, startcol, stopcol) + for i = startcol, stopcol do + target[i].text = source[i].text + target[i].attrs = source[i].attrs + end +end + +function Screen:_row_repr(row, attr_ids) + local rv = {} + local current_attr_id + for i = 1, self._width do + local attr_id = get_attr_id(attr_ids, row[i].attrs) + if current_attr_id and attr_id ~= current_attr_id then + -- close current attribute bracket, add it before any whitespace + -- up to the current cell + -- table.insert(rv, backward_find_meaningful(rv, i), '}') + table.insert(rv, '}') + current_attr_id = nil + end + if not current_attr_id and attr_id then + -- open a new attribute bracket + table.insert(rv, '{' .. attr_id .. ':') + current_attr_id = attr_id + end + if self._rows[self._cursor.row] == row and self._cursor.col == i then + table.insert(rv, '^') + else + table.insert(rv, row[i].text) + end + end + if current_attr_id then + table.insert(rv, '}') + end + -- return the line representation, but remove empty attribute brackets and + -- trailing whitespace + return table.concat(rv, '')--:gsub('%s+$', '') +end + + +function Screen:_current_screen() + -- get a string that represents the current screen state(debugging helper) + local rv = {} + for i = 1, self._height do + table.insert(rv, "'"..self:_row_repr(self._rows[i]).."'") + end + return table.concat(rv, '\n') +end + +function backward_find_meaningful(tbl, from) + for i = from or #tbl, 1, -1 do + if tbl[i] ~= ' ' then + return i + 1 + end + end + return from +end + +function new_cell_grid(width, height) + local rows = {} + for i = 1, height do + local cols = {} + for j = 1, width do + table.insert(cols, {text = ' ', attrs = {}}) + end + table.insert(rows, cols) + end + return rows +end + +function get_attr_id(attr_ids, attrs) + if not attr_ids then + return + end + for id, a in pairs(attr_ids) do + if a.bold == attrs.bold and a.standout == attrs.standout and + a.underline == attrs.underline and a.undercurl == attrs.undercurl and + a.italic == attrs.italic and a.reverse == attrs.reverse and + a.foreground == attrs.foreground and + a.background == attrs.background then + return id + end + end + return nil +end + +return Screen diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua new file mode 100644 index 0000000000..a1110b3231 --- /dev/null +++ b/test/functional/ui/screen_basic_spec.lua @@ -0,0 +1,224 @@ +local helpers = require('test.functional.helpers') +local Screen = require('test.functional.ui.screen') +local clear, feed, execute = helpers.clear, helpers.feed, helpers.execute +local insert = helpers.insert + +describe('Screen', function() + local screen + + before_each(function() + clear() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + describe('window', function() + describe('split', function() + it('horizontal', function() + execute('sp') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ~ | + ~ | + ~ | + ~ | + [No Name] | + :sp | + ]]) + end) + + it('horizontal and resize', function() + execute('sp') + execute('resize 8') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ~ | + ~ | + [No Name] | + :resize 8 | + ]]) + end) + + it('horizontal and vertical', function() + execute('sp', 'vsp', 'vsp') + screen:expect([[ + ^ | | | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [No Name] [No Name] | + | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ]]) + insert('hello') + screen:expect([[ + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + end) + end) + end) + + describe('tabnew', function() + it('creates a new buffer', function() + execute('sp', 'vsp', 'vsp') + insert('hello') + screen:expect([[ + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + execute('tabnew') + insert('hello2') + feed('h') + screen:expect([[ + 4+ [No Name] + [No Name] X| + hell^2 | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + execute('tabprevious') + screen:expect([[ + 4+ [No Name] + [No Name] X| + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + end) + end) + + describe('insert mode', function() + it('move to next line with <cr>', function() + feed('iline 1<cr>line 2<cr>') + screen:expect([[ + line 1 | + line 2 | + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + -- INSERT -- | + ]]) + end) + end) + + describe('command mode', function() + it('typing commands', function() + feed(':ls') + screen:expect([[ + | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls^ | + ]]) + end) + + it('execute command with multi-line output', function() + feed(':ls<cr>') + screen:expect([[ + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls | + 1 %a "[No Name]" line 1 | + Press ENTER or type command to continue^ | + ]]) + feed('<cr>') -- skip the "Press ENTER..." state or tests will hang + end) + end) +end) |