aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/functional/api/buffer_spec.lua5
-rw-r--r--test/functional/api/vim_spec.lua9
-rw-r--r--test/functional/clipboard/autoload/provider/clipboard.vim16
-rw-r--r--test/functional/clipboard/clipboard_provider_spec.lua141
-rw-r--r--test/functional/helpers.lua53
-rw-r--r--test/functional/legacy/026_execute_while_if_spec.lua18
-rw-r--r--test/functional/legacy/033_lisp_indent_spec.lua20
-rw-r--r--test/functional/ui/highlight_spec.lua184
-rw-r--r--test/functional/ui/input_spec.lua40
-rw-r--r--test/functional/ui/mouse_spec.lua157
-rw-r--r--test/functional/ui/screen.lua384
-rw-r--r--test/functional/ui/screen_basic_spec.lua224
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)